Sitemap
JavaScript in Plain English

New JavaScript and Web Development content every day. Follow to join our 3.5M+ monthly readers.

Reusable React Components

6 min readMar 20, 2024

--

If you’re here today, you’re likely eager to expand your React knowledge and skills, so let’s dive straight in. Our goal is to transform a simple React component, designed to display product details, into something that can be reused across an entire application.

import React, { useEffect } from "react";


const ProductDetails = ({ productId }) => {
const [product, setProduct] = useState(null);

// Dummy logic to set the product.
useEffect(() => {
if (productId) {
setProduct({
id: 1,
poster:
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQZl235HLv0P4CdlFjSeA11l42xWC1mdHH33w&usqp=CAU",
name: "Product Name",
price: "$8.00",
rating: 10,
});
}
}, [productId]);

return (
<div className="product__details">
<img
className="product__details__poster"
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className="product__details__name">{product?.name}</span>
<span className="product__details__price">{product?.price}</span>
<span className="product__details__rating">{product?.rating}</span>
</div>
);
};

export default ProductDetails;

At first glance, this component takes in a product ID, fetches a product, and uses that information to render the product details, applying specific CSS classes for styling. This approach is common but doesn’t offer much flexibility for various scenarios.

Before we proceed, it’s important to remember that making components endlessly extendable for all circumstances isn’t practical if they’re only used in a few places within your application. Instead, it’s wise to focus on reusability as your requirements evolve. If you foresee using the component in multiple areas, investing more effort into reusability is advisable.

When making components reusable there are three main points that I focus on.

  1. Styling
  2. Logic
  3. Content

Although styling and content are closely related, I prefer to distinguish between them when discussing reusability. So, how can we design our component’s styles to ensure it’s reusable? The answer lies in passing styles as props. There are more sophisticated strategies, which I’ll discuss later, but let’s start with the basics.

Styling

To make a component’s style adaptable, simply pass a prop containing class names to override the existing styles.

import React, { useEffect } from "react";

const ProductDetails = ({ productId, classes }) => {
const [product, setProduct] = useState(null);

// Dummy logic to set the product.
useEffect(() => {
if (productId) {
setProduct({
id: 1,
poster:
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQZl235HLv0P4CdlFjSeA11l42xWC1mdHH33w&usqp=CAU",
name: "Product Name",
price: "$8.00",
rating: 10,
});
}
}, [productId]);

return (
<div className={`product__details ${classes.root}`}>
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
</div>
);
};

export default ProductDetails;

If you wish to customize only the container and the product name’s styles, you can use the component like so:

<ProductDetails productId={1} classes={{root: "overriding classes", name: "overriding classes"}} />

This method is a straightforward way to adjust a component’s styling without duplicating code. For those looking to modify the entire content structure while maintaining the default if needed, the content section will cover that.

Logic

To make the component’s logic reusable, we can abstract it using a React hook, allowing the logic to be utilized anywhere in our React application.

import React, { useEffect } from "react";

const useProduct = (productId) => {
const [product, setProduct] = useState(null);
// Dummy logic to set the product.
useEffect(() => {
if (productId) {
setProduct({
id: 1,
poster:
"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQZl235HLv0P4CdlFjSeA11l42xWC1mdHH33w&usqp=CAU",
name: "Product Name",
price: "$8.00",
rating: 10,
});
}
}, [productId]);
return { product }
}

const ProductDetails = ({ productId, classes }) => {
const product = useProduct(productId)

return (
<div className={`product__details ${classes.root}`}>
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
</div>
);
};

export default ProductDetails;

Utilizing a hook can significantly streamline a React component, enabling us to apply the logic universally. While there are more sophisticated approaches, starting with a hook is effective. If similar logic is needed in various components, abstracting it into a hook can avoid the pitfalls of useEffect Hell. A side note: React 19 might see reduced reliance on useEffect for data fetching, leaning towards alternative methods. This shift underscores the advantage of hooks; they facilitate easier updates to project logic without the need to sift through components for specific logic. In my experience, managing multiple useEffect instances in a single project can complicate readability, making isolated hooks a cleaner solution.

Content

Addressing the complexity of reusability, particularly with content, can be challenging yet less frequently utilized. The key lies in cautiously passing children through props, allowing for content customization. It’s crucial to set clear requirements and limit adjustments to ensure the component remains sufficiently reusable without over-complication. React’s flexibility often tempts developers to push the bounds of reusability.

// You would call it by saying...
<ProductDetails productId={1}>
<>
{/* ANY CONTENT YOU WANT */}
</>
</ProductDetails>
const ProductDetails = ({ productId, classes, children }) => {
const product = useProduct(productId)


if (children) return children;

return (
<div className={`product__details ${classes.root}`}>
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
</div>
);
};

export default ProductDetails;

Pretty straightforward, you pass children through the props, then return it if it exist. But how do the children receive the product data?

Get Seth Heinzman’s stories in your inbox

Join Medium for free to get updates from this writer.

Since the children are passed as a function you can pass it as parameters.

// This is how you would call product details and pass content through the children's prop.
<ProductDetails productId={1}>
{(product) => <>{/* ANY CONTENT YOU WANT */}</>}
</ProductDetails>;
// OR 
<ProductDetails productId={1} children={(product) => <> {/* ANY CONTENT YOU WANT */} </>} />
const ProductDetails = ({ productId, classes, children }) => {
const product = useProduct(productId)

if (children) return children(product);

return (
<div className={`product__details ${classes.root}`}>
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
</div>
);
};


export default ProductDetails;

Passing content through props is straightforward. However, enabling children to access product data requires passing them as a function, allowing for parameter transmission. The next method separates the logic of the Product Details from the content. This method not only simplifies content adaptation but also enhances the component’s flexibility to be dismantled into logic and content-specific components, following a container pattern.

// This allows you to use the same logic but with different content.
<ProductDetailsContainer productId={id} children={(product) => <ProductDetails product={product}/>}/>
// With different styles.
<ProductDetailsContainer productId={id} children={(product) => <ProductDetails product={product} classes={{root: ""}}/>}/>
// With different component.
<ProductDetailsContainer productId={id} children={(product) => <OTHERCONTENT product={product}/>}/>
const ProductDetailsContainer = ({productId, children}) => {
const product = useProduct(productId);
// any other product logic.
return children(product);
}

export default ProductDetails;
const ProductDetails = ({ product, classes}) => {
return (
<div className={`product__details ${classes.root}`}>
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
</div>
);
};

export default ProductDetails;

I could stop here, which is where I would recommend stopping. But, I’ll go further to show you what taking it too far gets you. It’s not a problem to take it too far, it just depends on your requirements. 99% of the time you can accomplish what you need by using the strategy above. I would say 100%, I’m sure there’s a random use case I’m not thinking of so I’ll leave the padding.

Taking it too far

// This is how you would call product details and pass content through the children's prop.
<ProductDetails
productId={1}
classes={{ root: "" }}
parts={{ image: <CustomImageComponent />, name: <CustomNameComponent /> }}
/>;
const ProductDetails = ({ productId, classes, children, parts }) => {
const product = useProduct(productId);

if (children) return children(product);

return (
<div className={`product__details ${classes.root}`}>
{parts.image ? (
parts.image
) : (
<img
className={`product__details__poster ${classes.poster}`}
src={product?.poster}
alt={product?.name}
aria-label={product?.name}
/>
)}
{parts.name ? (
parts.name
) : (
<span className={`product__details__name ${classes.name}`}>
{product?.name}
</span>
)}
{parts.price ? (
parts.price
) : (
<span className={`product__details__price ${classes.price}`}>
{product?.price}
</span>
)}
{parts.rating ? (
parts.rating
) : (
<span className={`product__details__rating ${classes.rating}`}>
{product?.rating}
</span>
)}
</div>
);
};

export default ProductDetails;

This involves replacing each part of the component with a custom component if provided. It allows you to disable each one through classes, then if needed change how each section is presented. You can also pass props to these the same way you pass props through the children, but I figured I would leave something to the imagination. It’s not that complicated when dealing with a simple component, but managing a significantly larger component becomes difficult to read and manage. It’s just not worth the effort when the other examples get the job done.

Conclusion

Well, I hope you got something from this article. I didn’t expect it to be this long, there are plenty of other ways to make components reusable but this is the way I like doing them. Let me know if you have any questions or tips or mistakes that I made.

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

--

--