🧱 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...');
4
5 return (
6 <div>
7 <section>
8 <h1>Kanban board</h1>
9 <KanbanBoard />
10 <button onClick={createStory} className="button-create-issue">
11 Create Story
12 </button>
13 <button onClick={createBug} className="button-create-issue">
14 Create Bug
15 </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);
6
7const App = () => {
8 const createStory = () => console.log('Story created...');
9 const createBug = () => console.log('Bug created...');
10
11 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>
    20
    21<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);
    3
    4const increaseChipsCount = () => setChipsOrdered((count) => count + 1);
    5const decreaseChipsCount = () => setChipsOrdered((count) => count - 1);
    6
    7const 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';
    2
    3type HeadingLevel = 1 | 2 | 3 | 4;
    4
    5interface Props {
    6 headingLevel: HeadingLevel;
    7 headingID: `${string}-${string}`;
    8 headingTitle: string;
    9 className?: string;
    10 headingClassName?: string;
    11 children: ReactNode;
    12}
    13
    14const 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;
    23
    24 return (
    25 <section aria-labelledby={headingID} className={className}>
    26 <Heading id={headingID} className={headingClassName}>{headingTitle}</Heading>
    27 {children}
    28 </section>
    29 );
    30};
    31
    32export default SectionWithHeading;
    33
    34// App.tsx
    35
    36<SectionWithHeading
    37 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.
    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>
    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.
  • Removed the repeated JavaScript logic for updating the order count of menu items by using a single OrderCounter component.
    1import { FC, useState } from "react";
    2
    3interface Props {
    4 emoji: string;
    5 ariaLabel: string;
    6 orderText: string;
    7}
    8
    9const OrderCounter: FC<Props> = ({ emoji, orderText, ariaLabel }) => {
    10 const [orderedCount, setOrderedCount] = useState(0);
    11
    12 const increaseCount = () => setOrderedCount((count) => count + 1);
    13 const decreaseCount = () => {
    14 if (orderedCount === 0) return;
    15 setOrderedCount((count) => count - 1);
    16 };
    17
    18 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};
    31
    32export 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 interactions
10) => {
11 total.push(actualDuration);
12
13 const totalRenderTimeOver100Updates = total.reduce(
14 (partialSum, a) => partialSum + a,
15 0
16 );
17
18 const avgerageRenderTimeOver100Updates =
19 total.reduce((a, b) => a + b, 0) / total.length;
20
21 console.log({
22 totalRenderTimeOver100Updates,
23 avgerageRenderTimeOver100Updates,
24 total: total.length
25 });
26};
27
28<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 updates
2{
3 totalRenderTimeOver100Updates: 33.79999992251396,
4 avgerageRenderTimeOver100Updates: 0.3379999992251396,
5 total: 100
6}
7// Reloaded browser, initial mount & batch of 100 updates
8{
9 totalRenderTimeOver100Updates: 32.29999999701977,
10 avgerageRenderTimeOver100Updates: 0.3229999999701977,
11 total: 100
12}
13// Reloaded browser, initial mount & batch of 100 updates
14{
15 totalRenderTimeOver100Updates: 36.29999981820583,
16 avgerageRenderTimeOver100Updates: 0.36299999818205836,
17 total: 100
18}
19// Reloaded browser, initial mount & batch of 100 updates
20{
21 totalRenderTimeOver100Updates: 32.69999998807907,
22 avgerageRenderTimeOver100Updates: 0.3269999998807907,
23 total: 100
24}

The results in milliseconds for our second pass on the orders app with component composition:

1// Reloaded browser, initial mount & batch of 100 updates
2{
3 totalRenderTimeOver100Updates: 23.800000056624413,
4 avgerageRenderTimeOver100Updates: 0.23800000056624412,
5 total: 100
6}
7// Reloaded browser, initial mount & batch of 100 updates
8{
9 totalRenderTimeOver100Updates: 24.799999952316284,
10 avgerageRenderTimeOver100Updates: 0.24799999952316285,
11 total: 100
12}
13// Reloaded browser, initial mount & batch of 100 updates
14{
15 totalRenderTimeOver100Updates: 21.600000023841858,
16 avgerageRenderTimeOver100Updates: 0.21386138637467186,
17 total: 100
18}
19// Reloaded browser, initial mount & batch of 100 updates
20{
21 totalRenderTimeOver100Updates: 20.69999998807907,
22 avgerageRenderTimeOver100Updates: 0.2069999998807907,
23 total: 100
24}

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.

Definition

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);
5
6const 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 <SectionWithHeading
4 headingLevel={2}
5 headingID="burger-orders"
6 headingTitle="Burgers Counter"
7 >
8 <OrderCounter emoji="🍔" ariaLabel="burger emoji" orderText="Burgers" />
9 </SectionWithHeading>
10 <SectionWithHeading
11 headingLevel={2}
12 headingID="chip-orders"
13 headingTitle="Chips Counter"
14 >
15 <OrderCounter emoji="🍟" ariaLabel="chips emoji" orderText="Chips" />
16 </SectionWithHeading>
17 <SectionWithHeading
18 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 2026
🔰 Beginner friendly

Learn about scope chain, hoisting, lexical scoping and the key differences between var, let and const.

0
0
0
JAVASCRIPT
<SectionWithHeading
headingID="about"
headingTitle="About"
headingLevel={2}
/>

đŸŦ HTML Section Sugar

12 Apr 2022 â€ĸ 📖 5 min read â€ĸ Updated 15 Mar 2026
⚡ Lightning read

A lightweight React wrapper to create sections as document regions.

0
0
0
ACCESSIBILITY REACT