DEV Community

Nadia Makarevich
Nadia Makarevich

Posted on • Updated on • Originally published at developerway.com

React components composition: how to get it right

Image description

Originally published at 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.

const Button = ({ title, onClick }) => <button onClick={onClick}>{title}</button>;
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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

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:

const Navigation = () => {
  return (
    <>
      <Button onClick={onClickHandler}>Create</Button>
      ... // some other navigation code
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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 higher-order components, passing components as props or context, 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

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

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 codesandbox. 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:

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

Now letā€™s take a look at the new Topbar component implementation:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

export const Sidebar = () => {
  return (
    <div className="sidebar">
      <Header />
      <PlanningSection />
      <DevelopmentSection />
      other sidebar sections
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

See the full example in the codesandbox.

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

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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

      <ul className="section-menu">... all the menu items here</ul>
    </CollapsableSection>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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

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:

const JiraPageLayout = ({ children }) => {
  return (
    <div className="app">
      <Topbar />
      <div className="main-content">
        <Sidebar />
        <div className="page-content">{children}</div>
      </div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

export const JiraIssuePage = () => {
  return (
    <JiraPageLayout>
      <Issue />
    </JiraPageLayout>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

And our Sidebar component will turn into this:

export const Sidebar = () => {
  return (
    <DraggableSidebar>
      <Header />
      <PlanningSection />
      <DevelopmentSection />
      other Sections
    </DraggableSidebar>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 this codesandbox. And to compare the bad re-renders use case, see this codesandbox. 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: How to write performant React code: rules, patterns, do's and don'ts, Why custom react hooks could destroy your app performance, How to write performant React apps with Context

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?

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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:

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

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

And the section component becomes much simpler as a bonus:

const OtherSection = () => {
  return (
    <div className="sidebar-section">
      <ul className="section-menu">
        <li>
          <AddShortcutItem />
        </li>
      </ul>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

See it in the codesandbox.

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. The website has more articles like this šŸ˜‰

Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.

Top comments (14)

Collapse
 
lgibso34 profile image
Logan Gibson • Edited

My mind is blown. I did not know that passing the children prop prevented the children from re-rendering if the parent re-renders. I referred back to your article

How to write performant React code

and one of the ways React can re-render is

when a parent component re-renders

So my mind is absolutely blown that the children prop slightly invalidates the sentence above.

This stack overflow answer helped me understand why

I absolutely love your articles. They are so in depth and your topics are exactly what I want to read about. Thank you for taking the time to write these!

Collapse
 
adevnadia profile image
Nadia Makarevich

Thank you for such nice feedback šŸ„°
Gives me motivation to write more ā˜ŗļø

Collapse
 
micheledellaquila profile image
MicheleDellaquila • Edited

Nice article. I have a question.
I have this header:

header > div with icon theme and div with profile user
When click on div with profile user dropdown open.

I must divide in multiple components because the logic of open / close dropdown belongs profile not a header component ? Correct ?

Collapse
 
adevnadia profile image
Nadia Makarevich

The beauty of React is there are no strict rules, only recommendations and suggestions, and people can make their own decisions based on them and what makes sense for their application šŸ™‚

If the only thing that you have in your header is a div with an icon and the dropdown menu, I wouldn't bother with extraction - the component is going to be small enough to read through it anyway.

If you have or plan to have in the nearest future multiple items there, and the header is going to manage multiple states independent from each other, then extracting the logic of the dropdown would help with readability. And then yes, extracting it would make sense.

Collapse
 
micheledellaquila profile image
MicheleDellaquila

thank you for answering. In this moment my header manages open / close dropdown's state and with context the theme of application. I'm gonna include an icon that it will manage close or open sidebar. I think divided is perfect solution because i don't want to header manages this 3 different state, obviously as you said there aren't fixed rule. My problem were the size of component and managed this state / context. Congratulations again for the articles.

Collapse
 
elvezpablo profile image
Paul

Excellent article once again!

Collapse
 
gabrielmlinassi profile image
Gabriel Linassi • Edited

Really good. Thanks for sharing.
@adevnadia since you worked on Jira, I'd like to ask you a question I'm very curious. How do you handle modals? I know it can be very complex handle multiple modal to add/edit/delete. Is it something similar to github.com/eBay/nice-modal-react ?

And I'd love to see some more advanced patterns as well like:
1) compound composition: <Menu.Button></Menu.Button>
2) render propos: <Menu.Item>{({ active }) => <button className={clsx(active ? '' : '')} ></button>}</Menu.Item>
3) custom hooks: const { open, close, isOpen } = useModal
4) return component from custom hooks: const { Modal, open } = useModal

Collapse
 
adevnadia profile image
Nadia Makarevich

Hi @gabrielmlinassi ,

In Jira they use Atlaskit - Atlassian components library. That one: atlassian.design/components/modal-...

As for other composition patterns: you might be interested in this article: developerway.com/posts/react-compo..., I cover render props in detail there.

And this one for custom hooks:
developerway.com/posts/why-custom-..., it covers exactly the usecase of returning a component (Modal) from a hook. In short - not a good idea, prone to bugs and performance issues.

Collapse
 
skube profile image
skube

Great article! While I understand it wasn't really the focus, I think the code could be improved with the use of semantic HTML tags. šŸ˜ƒ

Collapse
 
adevnadia profile image
Nadia Makarevich

Next time for sure šŸ™‚

Collapse
 
drewberrysteph profile image
Drew Stifler

Couldn't agree more!
Mind if you share on how you will approach the folder/file structuring?

Collapse
 
adevnadia profile image
Nadia Makarevich

Hi @drewberrysteph, you mean for this particular code, or guidelines in general?

Collapse
 
fatihcandev profile image
Fatih

Hi Nadia! Great article as always. An article about the guidelines in general and your preferations in the subject would be awesome! Thanks for your time and effort šŸ™ŒšŸ»

Thread Thread
 
adevnadia profile image
Nadia Makarevich

Will do!