đ§ą React Component Composition
Introduction
The case study is a simple food order count app. We'll use component composition and memoisation to reduce re-renders and cut down on unnecessary garbage collection.
Component Composition in React
React by nature encourages us to use component composition over inheritance. Reacts composition model is provided to us through the special children prop. Component composition allows us to isolate our components into reusable blocks of UI code and clip them together to compose our application. In its simplest form, component composition enables components to render additional components as children.
Here are two small examples, with and without component composition.
Without Component Composition
1const App = () => {2 const createStory = () => console.log('Story created...');3 const createBug = () => console.log('Bug created...');45 return (6 <div>7 <section>8 <h1>Kanban board</h1>9 <KanbanBoard />10 <button onClick={createStory} className="button-create-issue">11 Create Story12 </button>13 <button onClick={createBug} className="button-create-issue">14 Create Bug15 </button>16 </section>17 </div>18 );19};
With Component Composition
1const Button = ({ onClick, children }) => (2 <button onClick={onClick} className="button-create-issue">3 {children}4 </button>5);67const App = () => {8 const createStory = () => console.log('Story created...');9 const createBug = () => console.log('Bug created...');1011 return (12 <div>13 <section>14 <h1>Kanban board</h1>15 <KanbanBoard />16 <Button onClick={createStory}>Create Story</Button>17 <Button onClick={createBug}>Create Bug</Button>18 </section>19 </div>20 );21};
In the example above, we've used component composition to create a reusable Button element. The text "Create ticket" is passed as a child element to our buttons children prop and rendered inside our JSX curley braces.
Creating and composing applications with components is second nature for React developers, but over the years I'm still seeing large chunks of UI code bundled into a single file. React's composition model is there to break things into smaller, reusable components.
Now that we understand component composition, let's jump into our case study and put things into practice.
Orders App - Make it work
It can be hard to know upfront what the file architecture and component composition of your new application or feature will look like ahead of time. You could find yourself down one of two frustrating paths.
The first path is over-engineering, creating everything as reusable components with many small fragmented files that are hard to follow. The second path is a few files with large chunks of UI code doing everything, not split up enough.
A previous mentor and friend taught me not to worry about making it perfect on the first pass. Focus on "making it work" before "making it good" and then "making it great!". By approaching our code in stages it's easier to tackle problems as they appear and avoid over-engineering or under-engineering.
As our first step towards "making it work" we've creating the UI and JavaScript logic for 3 order counters in a single file. We've got a few issues though.
- We are repeating our counter section, paragraph and button elements.1<section>2<h2>Burgers Counter</h2>3<p>4 <span role="img" aria-label="burger emoji">5 đ6 </span>{' '}7 Burgers ordered: {burgersOrdered}8</p>9<button onClick={increaseBurgersCount}>+</button>10<button onClick={decreaseBurgersCount}>-</button>11</section>12<section>13<h2>Chips Counter</h2>14<p>15 <span role="img" aria-label="chips emoji">16 đ17 </span>{' '}18 Chips ordered: {chipsOrdered}19</p>2021<button onClick={increaseChipsCount}>+</button>22<button onClick={decreaseChipsCount}>-</button>23</section>
- We are re-rendering our navigation and footer elements every time we update a counter. If we only want to update our burger orders, we're also re-rendering our chips and drinks sections and vice versa.
- We are repeating the JavaScript logic for updating the order count of menu items by using 3 functions.
- We are destroying and recreating each order count function every time our app re-renders. Garbage collection in JavaScript is slow!1const increaseBurgersCount = () => setBurgersOrdered((count) => count + 1);2const decreaseBurgersCount = () => setBurgersOrdered((count) => count - 1);34const increaseChipsCount = () => setChipsOrdered((count) => count + 1);5const decreaseChipsCount = () => setChipsOrdered((count) => count - 1);67const increaseDrinksCount = () => setDrinksOrdered((count) => count + 1);8const decreaseDrinksCount = () => setDrinksOrdered((count) => count - 1);
Let's clean things up in the next section.
Orders App - Make it good
Component Composition Cleanup
State lives inside each OrderCounter. Updating one counter doesn't re-render the other.
By breaking components out into smaller chunks of independent UI code and using component composition we have:
- Removed the duplication of our section, paragraph, and button elements by creating a reusable section component. Our section component uses component composition via the special children prop to render our OrderCounter component inside.1import React, { FC, ReactNode } from 'react';23type HeadingLevel = 1 | 2 | 3 | 4;45interface Props {6 headingLevel: HeadingLevel;7 headingID: `${string}-${string}`;8 headingTitle: string;9 className?: string;10 headingClassName?: string;11 children: ReactNode;12}1314const SectionWithHeading: FC<Props> = ({15 headingLevel,16 headingID,17 headingTitle,18 children,19 className = '',20 headingClassName = '',21}) => {22 const Heading = `h${headingLevel}` as keyof JSX.IntrinsicElements;2324 return (25 <section aria-labelledby={headingID} className={className}>26 <Heading id={headingID} className={headingClassName}>{headingTitle}</Heading>27 {children}28 </section>29 );30};3132export default SectionWithHeading;3334// App.tsx3536<SectionWithHeading37 headingLevel={2}38 headingID="burger-orders"39 headingTitle="Burgers Counter"40>41 <OrderCounter emoji="đ" orderText="Burgers" />42</SectionWithHeading>
- Reduced re-rendering to the affected area only. Instead of re-rendering 24 elements on each order count update, now we're only re-rendering the 5 elements inside our OrderCounter component.
Sure, it's not perfect. We could break this component up even more and use memoisation to prevent the buttons from re-rendering, but there is a balance to all of this. It's not about achieving perfection with minimal re-renders across our entire application. Chasing this may lead to many small fragmented files and wasted developer time for a fraction of a millisecond in performance gains.1<p>2 <span role="img" aria-label={ariaLabel}>3 {emoji}4 </span>{" "}5 {orderText} ordered: {orderedCount}6</p>7<button onClick={increaseCount}>+</button>8<button onClick={decreaseCount}>-</button> - Removed the repeated JavaScript logic for updating the order count of menu items by using a single OrderCounter component.1import { FC, useState } from "react";23interface Props {4 emoji: string;5 ariaLabel: string;6 orderText: string;7}89const OrderCounter: FC<Props> = ({ emoji, orderText, ariaLabel }) => {10 const [orderedCount, setOrderedCount] = useState(0);1112 const increaseCount = () => setOrderedCount((count) => count + 1);13 const decreaseCount = () => {14 if (orderedCount === 0) return;15 setOrderedCount((count) => count - 1);16 };1718 return (19 <>20 <p>21 <span role="img" aria-label={ariaLabel}>22 {emoji}23 </span>{" "}24 {orderText} ordered: {orderedCount}25 </p>26 <button onClick={increaseCount}>+</button>27 <button onClick={decreaseCount}>-</button>28 </>29 );30};3132export default OrderCounter;
Orders App - Profiling Renders
We can measure the difference in render times between the two versions of our orders app using the Profiler component in React. I tested the two apps in batches of 100 render updates. This testing included the initial time to mount the app.
1const total = [];2const callback = (3 id,4 phase,5 actualDuration,6 baseDuration,7 startTime,8 commitTime,9 interactions10) => {11 total.push(actualDuration);1213 const totalRenderTimeOver100Updates = total.reduce(14 (partialSum, a) => partialSum + a,15 016 );1718 const avgerageRenderTimeOver100Updates =19 total.reduce((a, b) => a + b, 0) / total.length;2021 console.log({22 totalRenderTimeOver100Updates,23 avgerageRenderTimeOver100Updates,24 total: total.length25 });26};2728<Profiler id="app" onRender={callback}>29 <div className="app">30 {/* ... the rest of the app */}31 </div>32</Profiler>}
The results in milliseconds for our first pass on the orders app without component composition:
1// Reloaded browser, initial mount & batch of 100 updates2{3 totalRenderTimeOver100Updates: 33.79999992251396,4 avgerageRenderTimeOver100Updates: 0.3379999992251396,5 total: 1006}7// Reloaded browser, initial mount & batch of 100 updates8{9 totalRenderTimeOver100Updates: 32.29999999701977,10 avgerageRenderTimeOver100Updates: 0.3229999999701977,11 total: 10012}13// Reloaded browser, initial mount & batch of 100 updates14{15 totalRenderTimeOver100Updates: 36.29999981820583,16 avgerageRenderTimeOver100Updates: 0.36299999818205836,17 total: 10018}19// Reloaded browser, initial mount & batch of 100 updates20{21 totalRenderTimeOver100Updates: 32.69999998807907,22 avgerageRenderTimeOver100Updates: 0.3269999998807907,23 total: 10024}
The results in milliseconds for our second pass on the orders app with component composition:
1// Reloaded browser, initial mount & batch of 100 updates2{3 totalRenderTimeOver100Updates: 23.800000056624413,4 avgerageRenderTimeOver100Updates: 0.23800000056624412,5 total: 1006}7// Reloaded browser, initial mount & batch of 100 updates8{9 totalRenderTimeOver100Updates: 24.799999952316284,10 avgerageRenderTimeOver100Updates: 0.24799999952316285,11 total: 10012}13// Reloaded browser, initial mount & batch of 100 updates14{15 totalRenderTimeOver100Updates: 21.600000023841858,16 avgerageRenderTimeOver100Updates: 0.21386138637467186,17 total: 10018}19// Reloaded browser, initial mount & batch of 100 updates20{21 totalRenderTimeOver100Updates: 20.69999998807907,22 avgerageRenderTimeOver100Updates: 0.2069999998807907,23 total: 10024}
In such a small application like this, the impact is tiny. However, we've managed to slice our render time by around 1/3, which for web applications with huge component trees, network requests, and lots of media content, would be a massive improvement.
Asides from the performance gains, we've also made our code much easier to read, maintain, test and scale.
Orders App - Make it great
Memoisation & Final Tidy
We still haven't addressed one issue though. We're still garbage collecting and recreating our increaseCount and decreaseCount functions every time our OrderCounter re-renders from a count state update.
We can fix this with memoisation by using the useCallback() hook provided to us by React.
The React useCallback Hook returns a memoized callback function. Think of memoization as caching a value so that it does not need to be recalculated. This allows us to isolate resource intensive functions so that they will not automatically run on every render. The useCallback Hook only runs when one of its dependencies update. (Source)
1const increaseCount = useCallback(2 () => setOrderedCount((count) => count + 1),3 []4);56const decreaseCount = useCallback(() => {7 if (orderedCount === 0) return;8 setOrderedCount((count) => count - 1);9}, [orderedCount]);
As a final tidy, let's move our order counters into a single OrderCounters component.
1const OrderCounters: FC = () => (2 <>3 <SectionWithHeading4 headingLevel={2}5 headingID="burger-orders"6 headingTitle="Burgers Counter"7 >8 <OrderCounter emoji="đ" ariaLabel="burger emoji" orderText="Burgers" />9 </SectionWithHeading>10 <SectionWithHeading11 headingLevel={2}12 headingID="chip-orders"13 headingTitle="Chips Counter"14 >15 <OrderCounter emoji="đ" ariaLabel="chips emoji" orderText="Chips" />16 </SectionWithHeading>17 <SectionWithHeading18 headingLevel={2}19 headingID="drinks-orders"20 headingTitle="Drinks Counter"21 >22 <OrderCounter emoji="đĨ¤" ariaLabel="drinks emoji" orderText="Drinks" />23 </SectionWithHeading>24 </>25);
Our final version with component composition and memoisation:
The main focus of this article was component composition, not memoisation. In the next article, we'll build out our menu items using progressive image loading and add additional features to showcase React memoisation techniques in more detail.
Was this useful?
Thanks for reading! Time for one more?
if (true) {var x = 1}console.log(x) // 1, var leaks out!
đĻ var, let & const
23 Feb 2023 âĸ đ 9 min read âĸ Updated 15 Mar 2026Learn about scope chain, hoisting, lexical scoping and the key differences between var, let and const.
<SectionWithHeadingheadingID="about"headingTitle="About"headingLevel={2}/>
đŦ HTML Section Sugar
12 Apr 2022 âĸ đ 5 min read âĸ Updated 15 Mar 2026A lightweight React wrapper to create sections as document regions.