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>;
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
</>
);
};
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>;
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
</>
);
};
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
</>
)
}
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 š
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 š ).
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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.
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
And our Sidebar
component will turn into this:
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 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>
);
};
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)} />}
</>
);
};
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>
);
};
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 (15)
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 articleand one of the ways React can re-render is
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!
Thank you for such nice feedback š„°
Gives me motivation to write more āŗļø
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 ?
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.
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.
Excellent article once again!
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
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.
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. š
Next time for sure š
Couldn't agree more!
Mind if you share on how you will approach the folder/file structuring?
Hi @drewberrysteph, you mean for this particular code, or guidelines in general?
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 šš»
Will do!
I translated this article here to Brazilian Portuguese.
Thanks, š