Keep a slow part of the page from rendering on every key you type. Move state down, or pass the slow part in as children. We count every render.
In Part 18 you saw that when a component’s state changes, React usually renders that component and every component inside it. Usually that’s fine, because renders are cheap. But sometimes one child is slow. Then every extra render of it costs real work.
This part shows the first fix to try. It needs no new React function. You only change where the state lives, or which component makes the slow element. Both ideas come from earlier parts. Part 9 showed how to keep state low. Part 2 showed that React can skip a child when its element is the same object as last time. Part 2 promised we’d come back to that idea here.
Both fixes are a kind of composition: building a bigger part of the page out of smaller components. Part 3 used that word for a component that takes children.
Try this first
Read this code. Don’t press Run yet.
import { useState } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
export default function App() {
const [name, setName] = useState('')
return (
<div>
<input aria-label="Your name" value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}!</p>
<SlowList />
</div>
)
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList is slow on purpose. Its for loop counts to 20 million before it returns anything. The loop does nothing useful. It stands for real slow work, like sorting a long list or drawing a big chart. The _ marks in 20_000_000 only make the number easier to read. JavaScript ignores them. toLocaleString('en-US') prints the number as “20,000,000”.
The name in the box has nothing to do with the list. So make a guess. You type two letters, “ab”. How many lines will the Console show?
Press Run, then type “ab” in the box.
The Console shows 6 lines. The first 2 are the first render. Strict Mode is on in the playground, so React calls each component twice in development, as Part 2 explained. Then each letter adds 2 more lines.
So each letter you type makes SlowList count to 20 million twice. The list on the page never changes. All of that work is thrown away.
Why the slow list renders
Go back to Part 18. Typing calls setName. That is a state change in App, so React renders App again.
App returns JSX with <SlowList /> in it. Each time App runs, that line makes a brand new element object. So React renders SlowList too. It doesn’t matter that SlowList has no props. Its parent rendered, and its element is new.
Part 18 listed two cases where React skips a child. One is the same element object as last time. The other is a set function that gets the same value. Neither case fits here.
Count the work, not the time
How slow is 20 million steps? That depends on your computer. A new laptop and an old phone give very different times. So in this part we don’t measure time. We count renders, and each render of SlowList is 20 million steps.
We typed three letters, “abc”, one key at a time, with React 19.3. These counts leave out the first render:
| Strict Mode | SlowList renders |
Steps of work |
|---|---|---|
| off | 3 | 60 million |
| on | 6 | 120 million |
A production build has no Strict Mode, so real users get the first row. One render per letter is still too many for a part of the page that never changes.
If typing in the box feels slow on your computer, this is why. If it doesn’t, the counts are still real. On a slower device, they turn into a delay.
Fix 1: move the state down
Only two things use name: the box and the “Hello” line. So put them, and the state, in a small component of their own:
import { useState } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function NameBox() {
const [name, setName] = useState('')
return (
<div>
<input aria-label="Your name" value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}!</p>
</div>
)
}
export default function App() {
return (
<div>
<NameBox />
<SlowList />
</div>
)
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
Run it and type “ab”. The Console shows only the 2 lines from the first render. Typing changes state in NameBox, so React renders NameBox and nothing else. Nothing triggered App: its state didn’t change, and it has no parent that rendered. And SlowList is not inside NameBox.
We also typed “abc”. SlowList rendered 0 times, with Strict Mode on and off.
This is the same move as in Part 9. There, a Sidebar stopped rendering when the note box got its own component. React’s docs list it as one way to avoid extra work: “Prefer local state and don’t lift state up any further than necessary.”
The page looks exactly the same as before. Only the way we split it into components changed.
When moving state down isn’t enough
Now change the example. The box holds a color name, and the color is used on the <div> around everything:
import { useState } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
export default function App() {
const [color, setColor] = useState('')
return (
<div style={{ color }}>
<input aria-label="Color" value={color} onChange={e => setColor(e.target.value)} />
<p>Type a color name, like red or blue.</p>
<SlowList />
</div>
)
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
style={{ color }} is short for style={{ color: color }}. Run it and type “red”. The sentence and the list turn red. A color set on a tag also applies to the text inside it, unless something inside sets its own color. That is how CSS works.
The Console shows 8 lines: 2 for the first render, and 2 for each of the three letters.
Can we move the state down? The <div> uses color, and the <div> holds SlowList. If we move the state into a new component, that component must return the <div>. Then it must write <SlowList /> inside the <div> too. We’d be back where we started.
So we need a second idea.
Fix 2: pass the slow part in as children
The new component keeps the state and the <div>. But it doesn’t write <SlowList /> itself. It takes whatever goes inside as children, and App puts the slow part there:
import { useState, type ReactNode } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function ColorBox({ children }: { children: ReactNode }) {
const [color, setColor] = useState('')
return (
<div style={{ color }}>
<input aria-label="Color" value={color} onChange={e => setColor(e.target.value)} />
{children}
</div>
)
}
export default function App() {
return (
<ColorBox>
<p>Type a color name, like red or blue.</p>
<SlowList />
</ColorBox>
)
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
Run it and type “red”. The text turns red as before. But the Console shows only the 2 lines from the first render. Typing three letters rendered SlowList 0 times, with Strict Mode on and off.
ColorBox is a wrapper: a component that draws something around other components, and doesn’t need to know what they are. Part 3’s Card was one too.
Why this works
Follow one letter.
- You type a letter.
setColorchanges state inColorBox. - React renders
ColorBox. It doesn’t renderApp, becauseApphas no state that changed. Rendering never goes up the tree. ColorBoxreturns its JSX. The{children}in it is thechildrenprop. That prop came fromApp, the last timeApprendered.Appdidn’t render again, so it made no new elements.childrenis the very same object as last time.- React sees the same element object, with no update of its own. It skips
SlowList.
We checked step 4. With Strict Mode off, we kept every children value that ColorBox got: one at the start and one per letter. All 4 were the same object.
The key question is: which component made the element? An element is made by the component whose JSX has the tag. In Fix 2, <SlowList /> is written in App‘s JSX, so App made it. ColorBox only decides where it goes.
Step through both cases:
Where the state lives decides what renders. Choose a case, then press play or step through it.
The same steps, in words:
- In the first case,
Appholdscolor, andAppwrites<SlowList />itself. In the second case,ColorBoxholdscolor, andAppmade<SlowList />and passed it in. - You type the last letter of “red”.
setColorasks React to render the component that holds the state. - First case: React calls
App, andAppmakes a new<SlowList />element. Second case: React callsColorBox, notApp, andchildrenis the same element as last time. - First case: a new element, so React calls
SlowList, which does 20 million steps. Second case: the same element, so React skips it. - In both cases, React commits the new style on the
<div>, and the list turns red.
We watched the page while typing in Fix 2. The list was the same <ul> on the page from start to end. None of React’s changes touched it. The list still turned red, because the color comes from the <div> around it.
An everyday example
Think of a picture frame with a light on top. You can turn the light up or down. Each time, the light changes, but nobody paints the picture again. The frame holds a picture someone else made.
ColorBox is the frame, and color is the light. SlowList is the picture. App made the picture and handed it over.
The exact version
Don’t think a child passed as children never renders again. A child passed in as children is skipped only when nothing new reaches it. It still renders if:
- the component that made it renders again (here,
App), because then it makes a new element; - the child’s own state changes;
- the child reads a context value that changes. Part 18 checked this, and Part 23 covers context.
The element must also come from above. If ColorBox itself wrote <SlowList />, the trick would be gone. You’ll see both of these under Common mistakes.
Elements in other props, too
children is only one prop. Any prop can hold an element. A wrapper can take several, one for each place on the page:
import { useState, type ReactNode } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function Title() {
console.log('Title renders')
return <h2>My list</h2>
}
function ColorBox({ title, children }: { title: ReactNode; children: ReactNode }) {
const [color, setColor] = useState('')
return (
<div style={{ color }}>
{title}
<input aria-label="Color" value={color} onChange={e => setColor(e.target.value)} />
{children}
</div>
)
}
export default function App() {
return (
<ColorBox title={<Title />}>
<SlowList />
</ColorBox>
)
}
Title renders
Title renders
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
title={<Title />} passes an element as a normal prop. App made it, so it works like children. Run it and type “red”. Neither Title nor SlowList logs again.
A prop like title is sometimes called a slot: a named place in a wrapper that the parent fills in. Part 45 is about this pattern.
When the slow part needs the changing value
Both fixes work because SlowList doesn’t need the changing value. What if part of it does?
First, look for a way to split. Say only one line needs the color, like “Color: red”. Put that small line in ColorBox, and keep the slow part in children. Practice 2 below does this. We typed “red” with Strict Mode off, and SlowList rendered 0 times.
But sometimes the slow work itself uses the value. Think of a long list that is sorted or picked by the color. Then there’s nothing to split. In this example, pretend that the loop uses the color:
import { useState } from 'react'
function SlowList({ color }: { color: string }) {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples in {color}</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function ColorBox() {
const [color, setColor] = useState('')
return (
<div style={{ color }}>
<input aria-label="Color" value={color} onChange={e => setColor(e.target.value)} />
<SlowList color={color} />
</div>
)
}
export default function App() {
return <ColorBox />
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
Now SlowList needs the color, so it must get color as a prop. Only the component that holds the state can pass it. So that component must write <SlowList color={color} />, and every letter makes a new element. Run it and type “red”: 8 lines, the same as with no fix at all.
That render is real work this time, because the slow part uses the new value. Composition can’t remove it.
React.memo doesn’t help here either. It skips a child when its props are equal to last time, and Part 20 covers it. But here the prop changes on every letter. We checked. We wrapped this SlowList in memo, and it still rendered once per letter with Strict Mode off. In the playground, that’s 2 lines per letter.
What’s left is to make the slow work itself cheaper. Part 21 covers useMemo, which can keep the result of slow work between renders when its inputs didn’t change.
Composition or React.memo?
React.memo can also stop the extra renders from “When moving state down isn’t enough”. We wrapped that SlowList, with no props, in memo. It rendered 0 times while we typed “red”. Part 20 shows how.
So why try composition first? Two of the ideas that React’s memo page gives to try before you use memo are these:
- “When a component visually wraps other components, let it accept JSX as children.”
- “Prefer local state and don’t lift state up any further than necessary.”
Those are Fix 2 and Fix 1. They need no extra function, and nothing to keep up to date as the code grows. The same page warns about memo. One value that is new on every render “is enough to break memoization for an entire component.”
There’s a bigger reason too. Composition is a way to design components first, and a speed trick second. A wrapper that takes children doesn’t need to know what goes inside it. So the code is often easier to read, and the saved renders come with no extra work. React’s page on context gives the same advice for a different problem: “Extract components and pass JSX as children to them.”
Still, measure first. As Part 18 said, most renders are cheap. Use these fixes when you have counted the renders and one child is truly slow. Part 18 shows ways to count them, like console.log and the Profiler in React Developer Tools.
Common mistakes
Thinking children always render with their parent
Part 18’s rule, “a child renders when its parent renders”, has exceptions. A child passed in as children is one of them. Some people learn the rule and then wrap every component in memo to be safe. Before you do, look at where the state lives and which component makes the slow element. Often moving one of them is enough.
Thinking children never render again
children is not a rule that always holds. It only helps when the component that made the element doesn’t render again. Here the state moved up into App, above the wrapper:
import { useState, type ReactNode } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function ColorBox({ color, onColor, children }: { color: string; onColor: (c: string) => void; children: ReactNode }) {
return (
<div style={{ color }}>
<input aria-label="Color" value={color} onChange={e => onColor(e.target.value)} />
{children}
</div>
)
}
export default function App() {
const [color, setColor] = useState('')
return (
<ColorBox color={color} onColor={setColor}>
<p>Type a color name, like red or blue.</p>
<SlowList />
</ColorBox>
)
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
Run it and type “red”: 8 lines. Each letter renders App, and App makes a new <SlowList /> element every time. The fix: keep the state inside the wrapper, as in Fix 2.
Making the slow element inside the wrapper that has the state
import { useState } from 'react'
function SlowList() {
let steps = 0
for (let i = 0; i < 20_000_000; i++) {
steps = steps + 1
}
console.log('SlowList renders after', steps.toLocaleString('en-US'), 'steps')
return (
<ul>
<li>Apples</li>
<li>Bread</li>
<li>Milk</li>
</ul>
)
}
function ColorBox() {
const [color, setColor] = useState('')
return (
<div style={{ color }}>
<input aria-label="Color" value={color} onChange={e => setColor(e.target.value)} />
<SlowList />
</div>
)
}
export default function App() {
return <ColorBox />
}
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
SlowList renders after 20,000,000 steps
This has a wrapper, so it looks like Fix 2. But <SlowList /> is written inside ColorBox, the component with the state. Every letter makes a new element, so you get 8 lines again. The fix: write {children} in ColorBox, and <SlowList /> in App.
Passing the component function instead of an element has the same problem. Say App writes <ColorBox List={SlowList} />, and ColorBox writes <List />. Then ColorBox makes the element, on every render. We tried it: SlowList rendered once per letter with Strict Mode off. Pass the element, <SlowList />, not the function, SlowList.
Splitting everything into tiny components
Once you know these fixes, you might split every part of the page into its own component, just in case. Don’t. Many tiny components with state spread around are harder to read. Most of them save renders that cost almost nothing.
Split where it makes the code clearer, or where you counted renders of a slow part, as Part 18 showed. Good places are often the same. One is a wrapper that draws a frame. Another is a small component that holds the state of one box.
Practice
Press Edit on any example above and try these. Strict Mode is on, so count the doubles.
- In “Try this first”, type “abc” instead of “ab”. How many lines does the Console show in all?
- In Fix 2, add
<p>Color: {color}</p>insideColorBox, just before{children}. Type “red”. DoesSlowListrender more now? - Start again from Fix 2. Move the
useStateup intoApp. PasscolorandonColor={setColor}toColorBox, and use them there. Type “red”. How many lines does the Console show? - Start again from Fix 2. Change the name of the
childrenprop tolist, and pass the slow part as<ColorBox list={<SlowList />} />. Move the<p>intoColorBox. Type “red”. How many lines now?
Answers
- 8 lines: 2 for the first render, then 2 for each of the three letters.
- No. The page shows “Color: red”, but the Console still has only the 2 lines from the first render. The new
<p>is part ofColorBox, which renders anyway.SlowListis still the samechildrenelement. - 8 lines. Now each letter renders
App, andAppmakes a new<SlowList />element each time. This is the code from “Thinking children never render again”. - 2 lines, only from the first render.
listworks likechildren, becauseAppstill makes the element.
Interview questions
Try to answer each one out loud before you open the answer.
A component has no props. Why does it re-render when its parent’s state changes?
A state change renders that component and the components inside it. The parent’s JSX makes a new element for the child on each render, so React renders the child too. Props don’t decide it. A child with no props renders the same way.
A strong answer names the exceptions. React skips a child when its element is the same object as last time. It skips a render when a set function gets the value the state already has. Then React may still call that component once more, but never its children, as Part 18 said. And React.memo skips a child whose props are equal.
What does “move state down” mean? When doesn’t it work?
It means putting state in the smallest component that uses it, so a state change renders only that small part. Say a text box’s value is used only by the box and one line of text. Then give those two their own component. The slow parts beside it stop rendering when you type.
It doesn’t work when a tag around the slow part uses the state. An example is a <div style={{ color }}> that holds the slow part. The component that owns the state must return the <div>, and the slow part with it. Then you need the second fix: pass the slow part in as children.
How does passing a component as children avoid a re-render?
The element is made by the component whose JSX has the tag. With <ColorBox><SlowList /></ColorBox> in App, App makes the SlowList element. When ColorBox‘s state changes, only ColorBox renders. App doesn’t, so children is the same object as last time. React sees the same element with no update of its own, and skips it.
Without memo, React doesn’t look inside the props to see if the values are equal. A new element always brings a new props object, even an empty one. Part 3 checked this. So only the same element object lets React skip.
A strong answer gives a number. In our test, with Strict Mode off, we typed three letters. The slow child rendered 0 times as children. It rendered 3 times when the wrapper wrote it itself.
Does a child passed as children never re-render?
No. It renders again when the component that made it renders, because that makes a new element. It also renders when its own state changes, or when a context value it reads changes. It works only when the component that made the element doesn’t render again.
Should you use composition or React.memo first?
Composition, when it fits. React’s memo page lists two ideas that often make memo not needed. Let wrappers accept JSX as children, and keep state local. Composition needs no extra function. And nothing breaks it later the way one new value can break memo. It often makes the code clearer too, because a wrapper doesn’t need to know what goes inside it.
A strong answer adds when memo is still the right tool. That’s when the component that has the state must make the slow child, and its props stay the same. It also says to measure first, because most renders are cheap.
What if the slow child needs the value that keeps changing?
First, split. If only a small part needs the value, give the value to that small part. Keep the slow part in children. If the slow work itself uses the value, composition can’t help. The component that holds the value must pass it as a prop, so it makes a new element each time. And the render is needed, because the slow work uses the new value. React.memo doesn’t help either, because the prop really changed. You can make the slow work smaller. Or you can keep its result with useMemo when its own inputs didn’t change. Some apps also show fewer items at once, which Part 35 covers.
Can you pass elements in props other than children?
Yes. Any prop can hold an element, like <Page header={<Header />} side={<Menu />} />. The wrapper puts each one where it belongs. These named places are often called slots. They save renders the same way children does, because the parent that wrote the tags made the elements.
Sources
- memo, react.dev: “When a component visually wraps other components, let it accept JSX as children”, “Prefer local state and don’t lift state up any further than necessary”, and that one value that is always new “is enough to break memoization for an entire component.”
- Before You memo(), Dan Abramov, 2021: the two fixes, “Move State Down” and “Lift Content Up”, with a slow child and a color box. Our
ColorBoxfollows its example. - Render and Commit, react.dev: what triggers a render, and that rendering goes down to the components inside.
- Passing Props to a Component, react.dev:
childrenand wrappers. - Passing Data Deeply with Context, react.dev: “Extract components and pass JSX as children to them.”
- StrictMode, react.dev: the extra render in development.
- color, MDN: the
colorof a tag is passed on to the text inside it (its “Inherited” row says “yes”). - Every render count, log and page check above comes from running React 19.3.0 for this post, in jsdom, with Strict Mode on and off.
- This part follows the Component Composition kata in react-katas.