This article will teach you how to use the concept of composability to take a React component and make it more reusable and extendable. As software scales and new features are constantly added, it’s inevitable that you will, at some point, need different components with identical functionality. Wouldn’t you rather consolidate that functionally—and make changes to a single component—instead of modifying the same code for multiple components? Yeah, me too.
So What Is Composability?
Composability is components of a system combining to create higher-level structures. This is a concept that works great with React because React is a component-based library that allows for self-contained components to be combined into entirely new, higher-level components. Let’s take a look at an example. We will start by creating a basic grid of elements.
import ProductCard from './ProductCard';
const productData = [ /* Product data... */ ];
const ProductGrid = ({ products }) => {
return (
<div className="grid">
{products.map(product => (
<ProductCard
name={product.name}
price={product.price}
image={product.image}
/>
))}</div>
)
}
const App = () => (
<div className="App">
<ProductGrid
products={productData}
/>
</div>
)
So, here we have:
productData
: This is an array of product data<ProductGrid />
: This component acceptsproductData
via props and generates a grid ofProductCard
components from the data<ProductCard />
: This is a presentational component that displays product information
And this is the result:
We have a nice grid of products here. Cool. Now what if we also want to make an image board?
We can create a new component, duplicate the functionality of the ProductGrid
component onto it, use a different UI component, and change the props to accommodate that UI component.
import ProductCard from './ProductCard;
import ImageCard from './ImageCard';
const ProductGrid = ({ products }) => {
return (
<div className="grid">
{products.map(product => (
<ProductCard
name={product.name}
price={product.price}
image={product.image}
/>
))}</div>
)
}
const ImageBoard = ({ images }) => {
return (
<div className="grid">
{images.map(data => (
<ImageCard name={data.name} image={data.image} details={data.details} />
))}</div>
)
}
const productData = [ /* Product data... */ ];
const imagesData = [ /* Images data... */ ];
const App = () => (
<div className="App">
<ProductGrid products={productData} />
<ImageBoard images={imagesData} />
</div>
)
Both components use the same logic. They both loop through an array of data passed via props, and render a UI component for each array item in a grid layout. The only difference between the two is that they use different UI components.
ProductGrid
rendersProductCard
UI componentsImageBoard
rendersImageCard
UI components
Simple enough, and it’s not a bad solution. However, there are issues that arise when we want to modify or extend the grid functionality of both of these components. If we want to modify the grid in any way or add the ability to pass in options like grid spacing and column count, we would have to update both components individually.
import ProductCard from './ProductCard';
import ImageCard from './ImageCard';
const ProductGrid = ({ products }) => {
return (
<div className="grid">
{products.map(product => (
<ProductCard
name={product.name}
price={product.price}
image={product.image}
/>
))}</div>
)
}
const ImageBoard = ({ images }) => {
return (
<div className="grid">
{images.map(data => (
<ImageCard
name={data.name}
image={data.image}
details={data.details}
/>
))}</div>
)
}
const productData = [ /* Product data... */ ];
const imagesData = [ /* Images data... */ ];
const App = () => (
<div className="App">
<ProductGrid
products={productData}
/>
<ImageBoard
images={imagesData}
/>
</div>
)
Making Our Code More Composable
Although we were able to extend each of our component’s grid functionality, we had to write the same code in two different components, which can become repetitive and error-prone as our app grows.
Since both the ProductGrid
and ImageBoard
components render elements in a grid layout, we could pull that functionality into its own component and pass in whatever components and data we need to build the end result that we want. This is where we get into the idea of composability.
Again, composability is components of a system combining to create higher-level structures. In our case, we want to make the grid functionality of the ProductGrid
its own component so that we can reuse that functionality in more than one specific case without replicating that same functionality for other components.
Now let’s update the ProductGrid
component to be more composable.
import ProductCard from './ProductCard';
const ProductGrid = ({ products }) => {
return (
<div className="grid">
{products.map(product => (
<ProductCard
name={product.name}
price={product.price}
image={product.image}
/>
))}</div>
)
}
First, let’s remove the UI component from the ProductGrid
component.
const ProductGrid = ({ products }) => (
<div className="grid">
{products.map(product => ())}</div>
);
Then, let’s add another prop called Element
. This is what we will use to pass in our UI component.
const ProductGrid = ({ products, Element }) => (
<div className="grid">
{products.map(product => ())}</div>
);
Next, we will take that Element
prop and return that as a component and pass in the data from products
as props. Let’s also change the prop name from products
to something more generic like data
.
We should also change the name of the component from ProductGrid
to Grid
because it is no longer supposed to render a specific kind of grid.
const Grid = ({ data, Element }) => {
return (
<div className="grid">
{data.map(data => {
return <Element {...data} />;
})}</div>
)
}
Doing this makes our grid component more composable because we can now use the same component in multiple cases and pass in whatever UI component we’d like to render as a grid.
import ProductCard from './ProductCard';
const productData = [/* Product data... */];
const ProductGrid = () => (
<Grid
data={productData}
Element={ProductCard}
/>
)
And this is the end result:
Conclusion
We identified a piece of code that could be reused in the ProductGrid
component, pulled the functionality from that component, put that code into its own component, and applied that component to our higher-level construct. I hope this article has given you some insight on how to add some of that sweet, sweet composability to your React code.