Leveraging Composition and Rendering Patterns in React

Leveraging Composition and Rendering Patterns in React
In this article, we explored how passing components as props can help prevent unnecessary re-renders in react. Now, let’s take this a step further and dive into a pattern that builds on this concept.
Compostion Pattern:
Let’s say we need to create a card component like the one shown above. The card consists of a header and a body (which includes a graph). We can break down the creation of this component into two parts:
Header: Contains the title
Graph: Includes the graph and a description
// TrackerCard Component
const TrackerCard = ({ header, graph }) => {
return (
<div className="card">
{header}
{graph}
</div>
);
};
// Dummy Graph Component
const Graph = ({ steps, color, total }) => {
return (
<ProgressBar color={color} total={total} current={steps} />
);
};
// GraphTracker Component that accepts a Graph prop
const GraphTracker = ({ GraphComponent, description }) => {
return (
<>
{GraphComponent}
<p>{description}</p>
</>
);
};
// App Component
const App = () => {
const header = <h2>Step Tracker</h2>;
const graph = (
<GraphTracker
GraphComponent={<Graph color="red" steps={7} total={10} />}
description="Keep moving! You're doing great."
/>
);
return (
<div>
<TrackerCard header={header} graph={graph} />
</div>
);
};
When building components, we often define multiple props. Sometimes, a user might forget to provide certain props. In such cases, we can use React.cloneElement
to modify the props of a component and add default values, ensuring the component behaves as expected even if some props are missing.
const TrackerCard = ({ header, graph }) => {
const defaultProps = {
color: 'Red',
description: ''
};
const newProps = {
...defaultProps,
...graph.props
};
const graphInstance = React.cloneElement(graph, newProps);
return (
<div className="card">
{header}
{graphInstance}
</div>
);
};
Prop Render Pattern:
What if the card looks like the one below? We’ve added a button that allows the user to increase their step count when clicked.
To achieve this, we need to add a button inside the GraphTracker
component.
// GraphTracker component that accepts a Graph prop
const GraphTracker = ({ GraphComponent, steps, description }) => {
return (
<>
{GraphComponent}
<p>{description}</p>
{/* Button can be added here */}
</>
);
};
If we add the button inside GraphTracker
, clicking it won’t actually increase the step count. That’s because the GraphComponent
is being passed as a prop from the parent, and the state controlling the steps isn't managed within GraphTracker
.
To solve this, we can move the button to the parent component and manage the steps
state there. This approach is known as state lifting—instead of defining steps
in the child, we define and manage it in the parent component
// App Component
const App = () => {
const [step, setStep] = useState(0);
const header = <h2>Step Tracker</h2>;
const graphTracker = (
<GraphTracker
GraphComponent={<Graph color="Red" steps={step} total={10} />}
description="Keep moving! You're doing great."
/>
);
return (
<div>
<TrackerCard header={header} graph={graphTracker} />
<button onClick={() => setStep((prev) => prev + 1)}>Increase Step</button>
</div>
);
};
This is where the render pattern comes into play. The code below can be written like this:
const GraphTracker = ({ GraphComponent, steps, description }) => {
const [step, setStep] = useState(steps || 0); // Initialize state with steps prop if provided
return (
<>
{GraphComponent(step)} {/* Pass the step state to GraphComponent */}
<p>{description}</p>
<button onClick={() => setStep((prev) => prev + 1)}>Increase Step</button> {/* Corrected onClick handler */}
</>
);
};
// GraphTracker can be used like this
const App = () => {
const header = <h2>Step Tracker</h2>;
const GraphTrackerComponent = (
<GraphTracker
GraphComponent={(step) => <Graph color="Red" steps={step} total={10} />} // Pass step as prop to Graph
description="Keep moving! You're doing great."
/>
);
return (
<div>
<TrackerCard header={header} graph={GraphTrackerComponent} />
</div>
);
};
In this solution, we’re leveraging the render prop pattern by passing Graph
Component
as a function to the GraphTracker
component. By doing this, we avoid the need for state lifting, which keeps the App
component cleaner and more focused. Instead of managing the state in the parent and passing it down, we encapsulate the state within GraphTracker
and simply pass the necessary props to the GraphComponent
. This approach makes the code more modular, flexible, and reduces clutter in the parent component.
When to use the Render Props Pattern
The render props pattern is useful for decoupling behavior from presentation, making components more flexible and reusable. In the context of our component, the key benefit is sharing state and logic between components while keeping their rendering customizable. For example, we can share the state of GraphTracker
with the Graph
component and avoid the need for state lifting. Another benefit is that we can dynamically change the graph without modifying the definition of GraphTracker
.
Subscribe to my newsletter
Read articles from Mohit Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
