Refactoring My React Product Page: From 367 Lines to 79


When I started working on my current project — it's sort of like an e-commerce platform, but not exactly — I followed what I was comfortable with.
At first, shoved all the data fetching, mutation logic, and component rendering into a single file. It worked for the MVP phase, but soon became painful to read, debug, or extend.
The breaking point? My Product.jsx
file ballooned to 367 lines. That was my wake-up call.
The Turning Point
I had this one file — a Product.jsx
page — that was doing literally everything:
Fetching product data
Fetching user info
Fetching reviews
Posting reviews
Managing review form state
Rendering all components
...and it was 367 lines long.
Not only that, but every time something changed, I had to dig through this monster just to make small updates. That’s when I finally said: “Okay, this needs a real cleanup.”
import React, { useEffect, useState } from "react";
import { useParams } from "react-router";
import { Loading } from "../components";
import api from "../utils/api";
import { MdAlternateEmail } from "react-icons/md";
import { FaPhoneFlip, FaStar, FaRegStar } from "react-icons/fa6";
import { IoLocationSharp, IoCloseCircle } from "react-icons/io5";
function Product() {
const { id } = useParams();
const [isPending, setIsPending] = useState(false);
const [product, setProduct] = useState(null);
const [src, setSrc] = useState(null);
const [user, setUser] = useState({});
const [reviews, setReviews] = useState([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [rating, setRating] = useState(0);
const [text, setText] = useState("");
const [uploading, setUploading] = useState(false);
const [reviewError, setReviewError] = useState(null);
const [reviewSuccess, setReviewSuccess] = useState(null);
const [reviewUser, setReviewUser] = useState({});
const [error, setError] = useState(null);
const rate = [1, 2, 3, 4, 5];
const fetchProduct = async () => {
setIsPending(true);
setError(null);
try {
const res = await api.get(`/business/products/${id}`);
if (res.status === 200) {
setProduct(res.data.data);
setSrc(res.data.data.images[0]);
fetchUser(res.data.data.user);
await fetchReviews(res.data.data._id);
// await fetchReviewUsers(res?.data?.data?.reviews);
}
} catch (error) {
console.log(
"Error while fetching products: ",
error.response.data.message
);
setError("Failed to fetch product data.");
} finally {
setIsPending(false);
}
};
const fetchUser = async (id) => {
try {
const res = await api.get(`/users/${id}`);
setUser(res.data.data);
} catch (error) {
console.log("Error while fetching user: ", error.response.data.message);
setError("Failed to fetch user!");
}
};
const fetchReviews = async (id) => {
try {
const res = await api.get(`/reviews/product/${id}`);
if (res.status === 200) {
await fetchReviewUsers(res.data.data);
setReviews(res.data.data);
}
} catch (error) {
console.log(
"Error while fetching reviews: ",
error.response.data.message
);
setError("Failed to fetch reviews.");
}
};
const fetchReviewUsers = async (reviews) => {
let users = {};
try {
await Promise.all(
reviews.map(async (review) => {
const res = await api.get(`/users/${review?.user}`);
if (res.status === 200) {
users[review.user] = {
profileImage: res.data.data.profileImage,
fullName: res.data.data.fullName,
};
}
})
);
setReviewUser(users);
} catch (error) {
console.log(
"Error while fetching review users: ",
error.response.data.message
);
setError("Failed to fetch review users.");
}
};
const handleReviewSubmit = async () => {
try {
setUploading(true);
const res = await api.post(
"reviews/add",
{
productId: id,
star: rating,
text,
},
{ withCredentials: true }
);
if (res.status === 200) {
setReviewSuccess("Review added successfully!");
setDialogOpen((prev) => !prev);
await fetchProduct();
}
} catch (error) {
console.log("Error while adding review: ", error);
setReviewError(error.response.data.message);
} finally {
setUploading(false);
}
};
useEffect(() => {
fetchProduct();
}, [id]);
if (error) {
return (
<div className="p-4 md:p-8">
<div className="container-3 p-4 text-center">
<span className="text-red-500">{error}</span>
</div>
</div>
);
}
if (isPending) return <Loading />;
return (
<div className="seven-xl">
{/* Here all the rendering logic and styling with tailwindcss */}
</div>
);
}
export default Product;
What I Learned
After reading and experimenting a bit, I came across the idea of combining:
React Query (for data fetching and caching)
Custom Hooks (to encapsulate logic)
This was game-changing for me.
📦 Here's What I Changed
Instead of doing everything in the Product.jsx
file, I now use hooks like:
const { data: product, isLoading, error } = useProduct(id);
const { data: user } = useUser(product?.user);
const { data: reviews = [] } = useReviews(product?._id);
const { data: reviewUser = {} } = useReviewUsers(reviews);
const { submitReview, isSubmitting, reviewError } = useSubmitReview(id);
Each of these lives in a clean, isolated custom hook — and under the hood, they all use React Query.
// src/hooks/useProduct.ts
import { useQuery } from '@tanstack/react-query';
import api from '../utils/api';
export const useProduct = (id) => {
return useQuery({
queryKey: ['product', id],
queryFn: async () => {
const res = await api.get(`/business/products/${id}`);
return res.data.data;
},
enabled: !!id,
});
};
import { useQuery } from "@tanstack/react-query";
import api from "../utils/api";
export const useReviews = (productId) => {
return useQuery({
queryKey: ['reviews', productId],
queryFn: async () => {
const res = await api.get(`/reviews/product/${productId}`);
return res.data.data;
},
enabled: !!productId,
});
};
import { useQuery } from "@tanstack/react-query";
import api from "../utils/api";
export const useReviewUsers = (reviews) => {
return useQuery({
queryKey: ['reviewUsers', reviews],
queryFn: async () => {
const users = {};
await Promise.all(
reviews.map(async (review) => {
const res = await api.get(`/users/${review.user}`);
users[review.user] = {
profileImage: res.data.data.profileImage,
fullName: res.data.data.fullName,
};
})
);
return users;
},
enabled: reviews.length > 0,
});
};
// src/hooks/useSubmitReview.ts
import { useMutation, useQueryClient } from "@tanstack/react-query";
import api from "../utils/api";
export const useSubmitReview = (productId) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ rating, text }) => {
return await api.post("reviews/add", {
productId,
star: rating,
text,
});
},
onSuccess: () => {
queryClient.invalidateQueries(['reviews', productId]);
queryClient.invalidateQueries(['product', productId]);
},
});
};
import { useQuery } from "@tanstack/react-query";
import api from "../utils/api";
export const useUser = (id) => {
return useQuery({
queryKey: ['user', id],
queryFn: async () => {
const res = await api.get(`/users/${id}`);
return res.data.data;
},
enabled: !!id,
});
};
That’s not it!
Even though after using custom hooks still the file looks mess because of the too many divs and tailwind classes. So I adopted a modular approach in which I divided all the parts of page into several components and created separate .jsx
files for them.
Now the final show down…
import { useParams } from "react-router-dom";
import { useState } from "react";
import ImageGallery from "../components/product/ImageGallery";
import ContactDetails from "../components/product/ContactDetails";
import ProductDescriptionAndOffers from "../components/product/ProductDescriptionAndOffers";
import ReviewsSection from "../components/product/ReviewsSection";
import ReviewDialog from "../components/product/ReviewDialog";
import {
useProduct,
useUser,
useReviews,
useReviewUsers,
useSubmitReview,
} from "../hooks";
function Product() {
const { id } = useParams();
const { data: product, isLoading, error } = useProduct(id);
const { data: user } = useUser(product?.user);
const { data: reviews = [] } = useReviews(product?._id);
const { data: reviewUser = {} } = useReviewUsers(reviews);
const { submitReview, isSubmitting, reviewError } = useSubmitReview(id);
const [dialogOpen, setDialogOpen] = useState(false);
const [rating, setRating] = useState(0);
const [text, setText] = useState("");
const [src, setSrc] = useState(product?.images?.[0]);
if (error) return <div className="p-4 text-red-500">{error.message}</div>;
if (isLoading) return <div>Loading...</div>;
return (
<div className="seven-xl">
<ReviewDialog
open={dialogOpen}
onClose={() => setDialogOpen(false)}
rating={rating}
setRating={setRating}
text={text}
setText={setText}
submitReview={() => submitReview(rating, text)}
isSubmitting={isSubmitting}
error={reviewError}
/>
<div>
<h2 className="text-2xl font-semibold tracking-wider">
Category | {product?.category} | {product?.title}
</h2>
</div>
<div className="sm:p-4 grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8 overflow-y-hidden">
<ImageGallery
images={product.images}
src={src}
setSrc={setSrc}
title={product.title}
/>
<ContactDetails user={user} />
<ProductDescriptionAndOffers
description={product?.desc}
offers={product?.offers}
/>
<ReviewsSection
rating={product?.overallRating}
reviews={reviews}
reviewUser={reviewUser}
onReviewAdd={() => setDialogOpen(true)}
/>
</div>
</div>
);
}
export default Product;
And the biggest shock?
📉 My file size dropped from 367 lines to just 79 lines.
I’m not exaggerating — that’s real.
A Quick Word on Props
One thing I was unsure about was whether it's okay to pass multiple props down to components. I used to think it looked messy, but honestly — if you're keeping things logical and grouped, it's completely fine.
For example, passing rating
, text
, setText
, submitReview
, etc., into a <ReviewDialog />
component feels natural now. But I also learned that if you're passing more than 4–5 related props, it might be worth grouping them or using a custom hook internally.
Final Thoughts
This experience completely changed how I approach React in larger apps. I used to rely way too much on Context, and I didn’t realize how powerful React Query + Custom Hooks can be when used properly.
If you’re in the same place I was — managing growing complexity with Context-only architecture — I highly recommend giving this stack a shot.
It’s not about being “correct” or “perfect” — it’s just about writing code that you can actually maintain and scale.
TL;DR
I used to use Context for everything.
My
Product.jsx
file hit 367 lines.I switched to React Query + Custom Hooks.
Now it’s 79 lines, easier to manage, and cleaner than ever.
Props are fine — just keep them clean and grouped.
Let me know if you’ve gone through a similar transition. I’d love to hear how others approached this problem!
Subscribe to my newsletter
Read articles from Kirtan Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
