React components composition: how to get it right
Tags: webdev, react, tutorial, javascript
Published: 2022-04-12 03:26:36


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3lpldl5ad57paun1lxrm.png)


Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

One of the most interesting and challenging things in React is not mastering some advanced techniques for state management or how to use Context properly. More complicated to get right is how and when we should separate our code into independent components and how to compose them properly. I often see developers falling into two traps: either they are not extracting them soon enough, and end up with huge components “monoliths” that do way too many things at the same time, and that are a nightmare to maintain. Or, especially after they have been burned a few times by the previous pattern, they extract components way too early, which results in a complicated combination of multiple abstractions, over-engineered code and again, a nightmare to maintain.

What I want to do today, is to offer a few techniques and rules, that could help identify when and how to extract components on time and how not to fall into an over-engineering trap. But first, let’s refresh some basics: what is composition and which compositions patterns are available to us?

## React components composition patterns

### Simple components

Simple components are a basic building block of React. They can accept props, have some state, and can be quite complicated despite their name. A `Button` component that accepts `title` and `onClick` properties and renders a button tag is a simple component.

```tsx
const Button = ({ title, onClick }) => <button onClick={onClick}>{title}</button>;
```

Any component can render other components - that’s composition. A `Navigation` component that renders that `Button` - also a simple component, that composes other components:

```tsx
const Navigation = () => {
  return (
    <>
      // Rendering out Button component in Navigation component. Composition!
      <Button title="Create" onClick={onClickHandler} />
      ... // some other navigation code
    </>
  );
};
```

With those components and their composition, we can implement as complicated UI as we want. Technically, we don’t even need any other patterns and techniques, all of them are just nice-to-haves that just improve code reuse or solve only specific use cases.

### Container components

Container components is a more advanced composition technique. The only difference from simple components is that they, among other props, allow passing special prop `children`, for which React has its own syntax. If our `Button` from the previous example accepted not `title` but `children` it would be written like this:

```tsx
// the code is exactly the same! just replace "title" with "children"
const Button = ({ children, onClick }) => <button onClick={onClick}>{children}</button>;
```

Which is no different from `title` from `Button` perspective. The difference is on the consumer side, `children` syntax is special and looks like your normal HTML tags:

```tsx
const Navigation = () => {
  return (
    <>
      <Button onClick={onClickHandler}>Create</Button>
      ... // some other navigation code
    </>
  );
};
```

Anything can go into `children`. We can, for example, add an `Icon` component there in addition to text, and then `Navigation` has a composition of `Button` and `Icon` components:

```tsx
const Navigation = () => {
  return (
    <>
      <Button onClick={onClickHandler}>
        <!-- Icon component is rendered inside button, but button doesn't know -->
        <Icon />
        <span>Create</span>
      </Button>
      ...
      // some other navigation code
    </>
  )
}
```

`Navigation` controls what goes into `children`, from `Button`’s perspective it just renders whatever the consumer wants.

We’re going to look more into practical examples of this technique further in the article.

There are other composition patterns, like <a href="https://www.developerway.com/posts/higher-order-components-in-react-hooks-era">higher-order components</a>, <a href="https://www.developerway.com/posts/react-component-as-prop-the-right-way">passing components as props</a> or <a href="https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context">context</a>, but those should be used only for very specific use cases. Simple components and container components are the two major pillars of React development, and it’s better to perfect the use of those before trying to introduce more advanced techniques.

Now, that you know them, you’re ready to implement as complicated UI as you can possibly need!

Okay, I’m joking, I'm not going to do a “how to draw an owl” type of article here 😅

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3zgp5uj2v1igg85wduaj.jpeg)


It’s time for some rules and guidelines so that we can actually ~~draw that owl~~ build complicated React apps with ease.

## When is it a good time to extract components?

The core React development and decomposition rules that I like to follow, and the more I code, the more strongly I feel about them, are:

- always start implementation from the top
- extract components only when there is an actual need for it
- always start from “simple” components, introduce other composition techniques only when there is an actual need for them

Any attempt to think “in advance” or start “bottom-up“ from small re-usable components always ends up either in over-complicated components API or in components that are missing half of the necessary functionality.

And the very first rule for when a component needs to be decomposed into smaller ones is **when a component is too big**. A good size for a component for me is when it can fit on the screen of my laptop entirely. If I need to scroll to read through the component’s code - it’s a clear sign that it’s too big.

Let’s start coding now, to see how can this looks in practice. We are going to implement a typical Jira page from scratch today, no less (well, sort of, at least we’re going to start 😅).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1izkmzd59d5e2gv27hy.png)


This is a screen of an issue page from my personal project where I keep my favourite recipes found online 🍣. In there we need to implement, as you can see:

- top bar with logo, some menus, “create” button and a search bar
- sidebar on the left, with the project name, collapsable “planning” and “development” sections with items inside (also divided into groups), with an unnamed section with menu items underneath
- a big “page content” section, where all the information about the current issue is shown

So let’s start coding all of this in just one big component to start with. It’s probably going to look something like this:

```tsx
export const JiraIssuePage = () => {
  return (
    <div className="app">
      <div className="top-bar">
        <div className="logo">logo</div>
        <ul className="main-menu">
          <li>
            <a href="#">Your work</a>
          </li>
          <li>
            <a href="#">Projects</a>
          </li>
          <li>
            <a href="#">Filters</a>
          </li>
          <li>
            <a href="#">Dashboards</a>
          </li>
          <li>
            <a href="#">People</a>
          </li>
          <li>
            <a href="#">Apps</a>
          </li>
        </ul>
        <button className="create-button">Create</button>
        more top bar items here like search bar and profile menu
      </div>
      <div className="main-content">
        <div className="sidebar">
          <div className="sidebar-header">ELS project</div>
          <div className="sidebar-section">
            <div className="sidebar-section-title">Planning</div>
            <button className="board-picker">ELS board</button>

            <ul className="section-menu">
              <li>
                <a href="#">Roadmap</a>
              </li>
              <li>
                <a href="#">Backlog</a>
              </li>
              <li>
                <a href="#">Kanban board</a>
              </li>
              <li>
                <a href="#">Reports</a>
              </li>
              <li>
                <a href="#">Roadmap</a>
              </li>
            </ul>

            <ul className="section-menu">
              <li>
                <a href="#">Issues</a>
              </li>
              <li>
                <a href="#">Components</a>
              </li>
            </ul>
          </div>
          <div className="sidebar-section">sidebar development section</div>
          other sections
        </div>
        <div className="page-content">... here there will be a lot of code for issue view</div>
      </div>
    </div>
  );
};
```

Now, I haven’t implemented even half of the necessary items there, not to mention any logic, and the component is already way too big to read through it in one glance. See it in <a href="https://codesandbox.io/s/jira-page-one-big-component-twe6l8?file=/src/jira-issue-page.tsx">codesandbox</a>. That’s good and expected! So before going any further, it’s time split it into more manageable pieces.

The only thing that I need to do for it, is just to create a few new components and copy-paste code into them. I don’t have any use-cases for any of the advanced techniques (yet), so everything is going to be a simple component.

I’m going to create a `Topbar` component, which will have everything topbar related, `Sidebar` component, for everything sidebar related, as you can guess, and `Issue` component for the main part that we’re not going to touch today. That way our main `JiraIssuePage` component is left with this code:

```tsx
export const JiraIssuePage = () => {
  return (
    <div className="app">
      <Topbar />
      <div className="main-content">
        <Sidebar />
        <div className="page-content">
          <Issue />
        </div>
      </div>
    </div>
  );
};
```

Now let’s take a look at the new `Topbar` component implementation:

```tsx
export const Topbar = () => {
  return (
    <div className="top-bar">
      <div className="logo">logo</div>
      <ul className="main-menu">
        <li>
          <a href="#">Your work</a>
        </li>
        <li>
          <a href="#">Projects</a>
        </li>
        <li>
          <a href="#">Filters</a>
        </li>
        <li>
          <a href="#">Dashboards</a>
        </li>
        <li>
          <a href="#">People</a>
        </li>
        <li>
          <a href="#">Apps</a>
        </li>
      </ul>
      <button className="create-button">Create</button>
      more top bar items here like search bar and profile menu
    </div>
  );
};
```

If I implemented all the items there (search bar, all sub-menus, icons on the right), this component also would’ve been too big, so it also needs to be split. And this one is arguably a more interesting case than the previous one. Because, technically, I can just extract `MainMenu` component from it to make it small enough.

```tsx
export const Topbar = () => {
  return (
    <div className="top-bar">
      <div className="logo">logo</div>
      <MainMenu />
      <button className="create-button">Create</button>
      more top bar items here like search bar and profile menu
    </div>
  );
};
```

But extracting only `MainMenu` made the `Topbar` component slightly _harder_ to read for me. Before, when I looked at the `Topbar`, I could describe it as “a component that implements various things in the topbar”, and focus on the details only when I need to. Now the description would be “a component that implements various things in the topbar AND composes some random `MainMenu` component”. The reading flow is ruined.

This leads me to my second rule of components decomposition: when extracting smaller components, don’t stop halfway. A component should be described either as a “component that implements various stuff” or as a “component that composes various components together”, not both.

Therefore, a much better implementatioin of the `Topbar` component would look like this:

```tsx
export const Topbar = () => {
  return (
    <div className="top-bar">
      <Logo />
      <MainMenu />
      <Create />
      more top bar components here like SearchBar and ProfileMenu
    </div>
  );
};
```

Much easier to read now!

And exactly the same story with the `Sidebar` component - way too big if I’d implemented all the items, so need to split it:

```tsx
export const Sidebar = () => {
  return (
    <div className="sidebar">
      <Header />
      <PlanningSection />
      <DevelopmentSection />
      other sidebar sections
    </div>
  );
};
```

See the full example in <a href="https://codesandbox.io/s/jira-page-splitting-into-components-yje3qe?file=/src/jira-issue-page.tsx">the codesandbox</a>.

And then just repeat those steps every time a component becomes too big. In theory, we can implement this entire Jira page using nothing more than simple components.

## When is it time to introduce Container Components?

Now the fun part - let’s take a look at when we should introduce some advanced techniques and why. Starting with Container components.

First, let’s take a look at the design again. More specifically - at the Planning and Development sections in the sidebar menu.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctc186xmpiwkujn89644.png)


Those not only share the same design for the title, but also the same behaviour: click on the title collapses the section, and in “collapsed” mode the mini-arrow icon appears. And we implemented it as two different components - `PlanningSection` and `DevelopmentSection`. I could, of course, just implement the “collapse” logic in both of them, it's just a matter of a simple state after all:

```tsx
const PlanningSection = () => {
  const [isCollapsed, setIsCollapsed] = useState(false);
  return (
    <div className="sidebar-section">
      <div onClick={() => setIsCollapsed(!isCollapsed)} className="sidebar-section-title">
        Planning
      </div>

      {!isCollapsed && <>...all the rest of the code</>}
    </div>
  );
};
```

But:

- it’s quite a lot of repetition even between those two components
- content of those sections is actually different for every project type or page type, so even more repetition in the nearest future

Ideally, I want to encapsulate the logic of collapsed/expanded behavior and the design for the title, while leaving different sections full control over the items that go inside. This is a perfect use case for the Container components. I can just extract everything from the code example above into a component and pass menu items as `children`. We’ll have a `CollapsableSection` component:

```tsx
const CollapsableSection = ({ children, title }) => {
  const [isCollapsed, setIsCollapsed] = useState(false);

  return (
    <div className="sidebar-section">
      <div className="sidebar-section-title" onClick={() => setIsCollapsed(!isCollapsed)}>
        {title}
      </div>

      {!isCollapsed && <>{children}</>}
    </div>
  );
};
```

and `PlanningSection` (and `DevelopmentSection` and all other future sections) will become just this:

```tsx
const PlanningSection = () => {
  return (
    <CollapsableSection title="Planning">
      <button className="board-picker">ELS board</button>

      <ul className="section-menu">... all the menu items here</ul>
    </CollapsableSection>
  );
};
```

A very similar story is going to be with our root `JiraIssuePage` component. Right now it looks like this:

```tsx
export const JiraIssuePage = () => {
  return (
    <div className="app">
      <Topbar />
      <div className="main-content">
        <Sidebar />
        <div className="page-content">
          <Issue />
        </div>
      </div>
    </div>
  );
};
```

But as soon as we start implementing other pages that are accessible from the sidebar, we’ll see that they all follow exactly the same pattern - sidebar and topbar stay the same, and only the “page content” area changes. Thanks to the decomposition work we did before we can just copy-paste that layout on every single page - it’s not that much code after all. But since all of them are exactly the same, it would be good to just extract the code that implements all the common parts and leave only components that change to the specific pages. Yet again a perfect case for the “container” component:

```tsx
const JiraPageLayout = ({ children }) => {
  return (
    <div className="app">
      <Topbar />
      <div className="main-content">
        <Sidebar />
        <div className="page-content">{children}</div>
      </div>
    </div>
  );
};
```

And our `JiraIssuePage` (and future `JiraProjectPage`, `JiraComponentsPage`, etc, all the future pages accessible from the sidebar) becomes just this:

```tsx
export const JiraIssuePage = () => {
  return (
    <JiraPageLayout>
      <Issue />
    </JiraPageLayout>
  );
};
```

If I wanted to summarise the rule in just one sentence, it could be this: extract Container components when there is a need to share some visual or behavioural logic that wraps elements that still need to be under “consumer” control.

### Container components - performance use case

Another very important use case for Container components is improving the performance of components. Technically performance is off-topic a bit for the conversation about composition, but it would be a crime not to mention it here.

In actual Jira the Sidebar component is draggable - you can resize it by dragging it left and right by its edge. How would we implement something like this? Probably we’d introduce a `Handle` component, some state for the `width` of the sidebar, and then listen to the “mousemove” event. A rudimentary implementation would look something like this:

```tsx
export const Sidebar = () => {
  const [width, setWidth] = useState(240);
  const [startMoving, setStartMoving] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    const changeWidth = (e: MouseEvent) => {
      if (!startMoving) return;
      if (!ref.current) return;

      const left = ref.current.getBoundingClientRect().left;
      const wi = e.clientX - left;

      setWidth(wi);
    };

    ref.current.addEventListener('mousemove', changeWidth);

    return () => ref.current?.removeEventListener('mousemove', changeWidth);
  }, [startMoving, ref]);

  const onStartMoving = () => {
    setStartMoving(true);
  };

  const onEndMoving = () => {
    setStartMoving(false);
  };

  return (
    <div className="sidebar" ref={ref} onMouseLeave={onEndMoving} style={{ width: `${width}px` }}>
      <Handle onMouseDown={onStartMoving} onMouseUp={onEndMoving} />
      ... the rest of the code
    </div>
  );
};
```

There is, however, a problem here: every time we move the mouse we trigger a state update, which in turn will trigger re-rendering of the entire `Sidebar` component. While on our rudimentary sidebar it’s not noticeable, it could make the “dragging” of it visibly laggy when the component becomes more complicated. Container components are a perfect solution for it: all we need is to extract all the heavy state operations in a Container component and pass everything else through `children`.

```tsx
const DraggableSidebar = ({ children }: { children: ReactNode }) => {
  // all the state management code as before

  return (
    <div
      className="sidebar"
      ref={ref}
      onMouseLeave={onEndMoving}
      style={{ width: `${width}px` }}
    >
      <Handle onMouseDown={onStartMoving} onMouseUp={onEndMoving} />
      <!-- children will not be affected by this component's re-renders -->
      {children}
    </div>
  );
};
```

And our `Sidebar` component will turn into this:

```tsx
export const Sidebar = () => {
  return (
    <DraggableSidebar>
      <Header />
      <PlanningSection />
      <DevelopmentSection />
      other Sections
    </DraggableSidebar>
  );
};
```

That way `DraggableSidebar` component will still re-render on every state change, but it will be super cheap since it’s just one div. And everything that is coming in `children` will not be affected by this component’s state updates.

See all the examples of container components in <a href="https://codesandbox.io/s/jira-page-container-components-wrb88p?file=/src/components/sidebar.tsx">this codesandbox</a>. And to compare the bad re-renders use case, <a href="https://codesandbox.io/s/jira-page-container-components-with-bad-rerenders-6ipo1x?file=/src/components/sidebar.tsx">see this codesandbox</a>. Pay attention to the console output while dragging the sidebar in those examples - `PlanningSection` component logs constantly in the “bad” implementation and only once in the “good” one.

And if you want to know more about various patterns and how they influence react performance, you might find those articles interesting: <a href="https://www.developerway.com/posts/how-to-write-performant-react-code">How to write performant React code: rules, patterns, do's and don'ts</a>, <a href="https://www.developerway.com/posts/why-custom-react-hooks-could-destroy-your-app-performance">Why custom react hooks could destroy your app performance</a>, <a href="https://www.developerway.com/posts/how-to-write-performant-react-apps-with-context">How to write performant React apps with Context</a>

## Does this state belong to this component?

Another thing, other than size, that can signal that a component should be extracted, is state management. Or, to be precise, state management that is irrelevant to the component’s functionality. Let me show you what I mean.

One of the items in the sidebar in real Jira is “Add shortcut” item, which opens a modal dialog when you click on it. How would you implement it in our app? The modal dialog itself is obviously going to be its own component, but where you’d introduce the state that opens it? Something like this?

```tsx
const SomeSection = () => {
  const [showAddShortcuts, setShowAddShortcuts] = useState(false);

  return (
    <div className="sidebar-section">
      <ul className="section-menu">
        <li>
          <span onClick={() => setShowAddShortcuts(true)}>Add shortcuts</span>
        </li>
      </ul>
      {showAddShortcuts && <ModalDialog onClose={() => setShowAddShortcuts(false)} />}
    </div>
  );
};
```

You can see something like this everywhere, and there is nothing criminal in this implementation. But if I was implementing it, and if I wanted to make this component perfect from the composition perspective, I would extract this state and components related to it outside. And the reason is simple - this state has nothing to do with the `SomeSection` component. This state controls a modal dialog that appears when you click on shortcuts item. This makes the reading of this component slightly harder for me - I see a component that is “section”, and next line - some random state that has nothing to do with “section”. So instead of the implementation above, I would extract the item and the state that actually belongs to this item into its own component:

```tsx
const AddShortcutItem = () => {
  const [showAddShortcuts, setShowAddShortcuts] = useState(false);

  return (
    <>
      <span onClick={() => setShowAddShortcuts(true)}>Add shortcuts</span>
      {showAddShortcuts && <ModalDialog onClose={() => setShowAddShortcuts(false)} />}
    </>
  );
};
```

And the section component becomes much simpler as a bonus:

```tsx
const OtherSection = () => {
  return (
    <div className="sidebar-section">
      <ul className="section-menu">
        <li>
          <AddShortcutItem />
        </li>
      </ul>
    </div>
  );
};
```

See it in <a href="https://codesandbox.io/s/jira-page-add-shortcuts-dialog-wb3iq8?file=/src/components/sidebar.tsx">the codesandbox</a>.

By the same logic, in the `Topbar` component I would move the future state that controls menus to a `SomeDropdownMenu` component, all search-related state to `Search` component, and everything related to opening “create issue” dialog to the `CreateIssue` component.

## What makes a good component?

One last thing before closing for today. In the summary I want to write “the secret of writing scalable apps in React is to extract good components at the right time”. We covered the “right time” already, but what exactly is a “good component”? After everything that we covered about composition by now, I think I’m ready to write a definition and a few rules here.

A “good component” is a component that I can easily read and understand what it does from the first glance.

A “good component” should have a good self-describing name. `Sidebar` for a component that renders sidebar is a good name. `CreateIssue` for a component that handles issue creation is a good name. `SidebarController` for a component that renders sidebar items specific for “Issues” page is not a good name (the name indicates that the component is of some generic purpose, not specific to a particular page).

A “good component” doesn’t do things that are irrelevant to its declared purpose. `Topbar` component that only renders items in the top bar and controls only topbar behaviour is a good component. `Sidebar` component, that controls the state of various modal dialogs is not the best component.

## Closing bullet points

Now I can write it 😄! The secret of writing scalable apps in React is to extract good components at the right time, nothing more.

**What makes a good component?**

- size, that allows reading it without scrolling
- name, that indicates what it does
- no irrelevant state management
- easy-to-read implementation

**When is it time to split a component into smaller ones?**

- when a component is too big
- when a component performs heavy state management operations that might affect performance
- when a component manages an irrelevant state

**What are the general components composition rules?**

- always start implementation from the very top
- extract components only when you have an actual usecase for it, not in advance
- always start with the Simple components, introduce advanced techniques only when they are actually needed, not in advance

That is all for today, hope you enjoyed the reading and found it useful! See ya next time ✌🏼

...

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.