When React re-renders
See what makes a React component render again: its state, its parent, or a context. Learn why a re-render isn't a DOM change, and when renders cost something.
- 01Name what makes a component renderIts own state, its parent rendering, or a context it reads, and know that the same props don't stop a render while the same state does.
- 02Tell a render from a change to the pageCount renders and DOM changes side by side, and explain why many renders can end in one change, or none.
- 03Decide when a render mattersSpot a render slow enough to feel, see renders with React DevTools, and fix one by moving state down or passing children.
A gift note that renders the whole page
This lesson opens the Performance stage. Before you make anything faster, you need to see what React is doing, and the first thing to see is when a component renders again: when React calls your component function another time to get a new result. The Render and commit lesson showed the steps of one update; this one counts them.
Here is a tea shop with a gift note. Each component shows a small counter, from the RenderCount.jsx tab, of how many times it has rendered, and its box flashes blue each time. You don't need to read that tab to use it. Click in the box and type Hi:
import { useState } from "react";import RenderCount from "./RenderCount.jsx"; function Header() { return <h3 className="box">Tea shop <RenderCount /></h3>;} function Bestsellers() { return <p className="box">Bestsellers: green tea, chai <RenderCount /></p>;} export default function App() { const [note, setNote] = useState(""); return ( <div className="box"> <Header /> <input placeholder="Gift note" value={note} onChange={(event) => setNote(event.target.value)} /> <p>Note: {note} <RenderCount /></p> <Bestsellers /> </div> );}Line 13 keeps the note in App's state, and line 18 calls setNote on every key. The counter on line 19 sits in App's own paragraph, so it counts App's renders. Header (lines 4 to 6) and Bestsellers (lines 8 to 10) have their own. They all start at renders: 1, the first render.
After Hi, all three say renders: 3. Each key changed App's state, so React rendered App, and then Header and Bestsellers, which App returns on lines 17 and 20. They rendered twice more, though they showed exactly the same text every time.
note, which lives in App. React renders App, and then every component App returns, all the way down, even Header and Bestsellers, which show the same as before.Is that a problem? Usually not. These components take a tiny fraction of a millisecond to render, and React only changes the page where the result differs. This lesson shows what makes a component render, why a render isn't a change to the page, and how to tell when a render costs enough to fix.
An author changes one word in a chapter. The proofreader doesn't know which pages it affects, so she reads the whole chapter again. Then the printer reprints only the page where the word changed. For a short chapter, the reading is quick. For a very long one, everyone waits for her.
- In real life: An author changes one word in a chapter
- In React: A state change in
App - In real life: The proofreader reads the whole chapter again
- In React: React renders
Appand every component inside it - In real life: The printer reprints only the page with the new word
- In React: React changes only the DOM that differs
- In real life: For a short chapter, the reading takes no time at all
- In React: For small components, renders cost nothing you'd notice
A component renders again when its state changes, when its parent renders, or when a context it reads changes. A render only calls your function; React then changes the page only where the result differs. Most renders are cheap. Fix the ones that are slow enough to feel.
Three reasons a component renders
After the first render, there are exactly three reasons React calls a component again. This shop shows two of them; the gift note above showed the third. Press Like, then Add to cart twice, and watch every counter:
import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const CartContext = createContext(0); function CartProvider({ children }) { const [count, setCount] = useState(0); return ( <CartContext value={count}> <button onClick={() => setCount(count + 1)}>Add to cart</button> {children} </CartContext> );} function CartBadge() { const count = useContext(CartContext); return <p className="box">Cart: {count} <RenderCount /></p>;} function LikeButton() { const [liked, setLiked] = useState(false); return ( <button className="box" onClick={() => setLiked(!liked)}> {liked ? "Liked" : "Like"} <RenderCount /> </button> );} function ProductCard() { return ( <div className="box"> Green tea <RenderCount /> <LikeButton /> </div> );} export default function App() { return ( <CartProvider> <CartBadge /> <ProductCard /> </CartProvider> );}- Its own state changes.
LikeButton(lines 21 to 28) keepslikedon line 22. Like changes it, and onlyLikeButton's counter goes up.ProductCard, its parent, stays at 1: a render goes down from the component whose state changed, never up. - Its parent renders. That's the gift note:
Apprendered, so everything it returned rendered. - A context it reads changes.
CartProvider(lines 6 to 14) keeps the count on line 7 and provides it on line 9.CartBadgereads it withuseContexton line 17, so after each Add to cart,CartBadgerenders:renders: 3after two clicks.
Look at ProductCard and LikeButton after Add to cart: still the same. CartProvider rendered, but it shows its children on line 11, and those elements were made by App on lines 42 and 43. App didn't render, so they're the same elements as last time, and React skips them. Only components that read the context render because of it, as the context lesson showed. (If you want to see the check React makes to skip them, the Reconciliation and the diffing algorithm lesson, in the Under the hood stage, reads it in React's code.)
How to read the table: each row is something that happens, what renders because of it, and where you can see it. The last row is the one exception, coming up in the next section.
| What happens | What renders | Where you see it |
|---|---|---|
| Its own state changes | That component, and every component it returns, all the way down. | Like renders only LikeButton. |
| Its parent renders | Every child the parent returns, even when its props are the same as last time. | Header and Bestsellers as you type a note. |
| A context it reads changes | Every component that reads that context, even when the components between didn't render. | CartBadge after Add to cart. |
| Its state is set to the value it already has | Nothing. React compares with Object.is and skips the render. | Set the same quantity. |
The playground puts all of this in one shop, like React DevTools would show it. Every box is a component. Type in the note, press Add to cart, and star a tea, then switch where the note's state lives:
function ShopPage() {
const [note, setNote] = useState("");
return (
<>
<Header />
<GiftNote note={note} onChange={setNote} />
<CartProvider>
<CartBadge />
<ProductList />
</CartProvider>
</>
);
}note in ShopPageNote:
Type in the gift note, press Add to cart, or star a tea. Each box flashes when its component renders, and its counter goes up.
With the note's state in ShopPage, one key renders all nine components, for one change on the page: the note. Move the state into GiftNote and the same key renders one. Add to cart renders CartProvider and CartBadge, and a star renders only its row. Clear note on an empty note renders nothing, which is the exception the next section explains.
The same props don't stop a render
It's natural to think a child renders only when its props change. It doesn't work that way. Here ShippingNote gets the same prop every time. Press Add one a few times:
import { useState } from "react";import RenderCount from "./RenderCount.jsx"; function ShippingNote({ country }) { return <p className="box">Ships to {country} <RenderCount /></p>;} export default function App() { const [quantity, setQuantity] = useState(1); return ( <div className="box"> <button onClick={() => setQuantity(quantity + 1)}>Add one</button> <button onClick={() => setQuantity(quantity)}>Set the same quantity</button> <p>Green tea × {quantity} <RenderCount /></p> <ShippingNote country="India" /> </div> );}Line 16 passes country="India", the same string on every render. Still, every Add one (line 13) makes ShippingNote's counter go up with App's. When a parent renders, React renders its children without comparing their props one by one. That's on purpose: most renders are cheap, and comparing every prop of every component would take time too.
You can ask React to compare: memo wraps a component so it skips the render when every prop is the same as last time. Press Edit & run, change line 1 to import { memo, useState } from "react";, line 4 to const ShippingNote = memo(function ShippingNote({ country }) {, and line 6 to });. Now Add one leaves ShippingNote at renders: 1. The next lesson, memo, useMemo and useCallback, covers memo and why it often doesn't help, and the React Compiler adds it for you.
State is different. Press Set the same quantity (line 14): it sets quantity to the value it already has, and nothing renders. Before React renders for a state change, it compares the new state with the old one using Object.is, and skips the render when they're the same. (Rarely, React calls the component once before it notices, but it still skips the children. The What setState really does lesson, in the Under the hood stage, shows why.) That's why you must make a new object or array to update state, as Updating objects in state explains: a changed object that is the same object looks unchanged.
A render is not a change to the page
A render only means React called your function. What reaches the page is decided after, in the commit. This example adds a second tab, watchDom.js: it prints a DOM: line in the console for every change React makes to the page, and shades the element around it orange, while the counters still flash blue. Press Add one:
import { useState } from "react";import RenderCount from "./RenderCount.jsx";import "./watchDom.js"; function Header() { return <h3 className="box">Tea shop <RenderCount /></h3>;} function Price({ quantity }) { return <p className="box">Total: ${quantity * 4} <RenderCount /></p>;} function Delivery() { return <p className="box">Free delivery over $20 <RenderCount /></p>;} export default function App() { const [quantity, setQuantity] = useState(1); return ( <div className="box"> <Header /> <button onClick={() => setQuantity(quantity + 1)}>Add one</button> <Price quantity={quantity} /> <Delivery /> <RenderCount /> </div> );}Line 3 imports watchDom.js. Line 18 keeps the quantity, and line 23's button adds one. Four counters go up: App's on line 26, and Header, Price, and Delivery, which App returns on lines 22, 24, and 25. Four boxes flash blue. But the console adds one line, DOM: text "4" → "8", and only the total's paragraph, which holds that text, turns orange. (The line before it, DOM: added <div>, is the first render putting the page in place.)
React compared each component's new result with its last one. Header and Delivery returned the same elements, so React left their DOM alone. In Price (line 10), only the number differs, so React changed that one text node, not even the whole paragraph.
How to read the table: each row asks one question of the render and of the commit.
| Question | Render | Commit |
|---|---|---|
| What happens? | React calls your components to get a new result. | React changes the page where the new result differs from the last one. |
| How much of it? | The component that changed and every component below it. | Only what differs, often one text, sometimes nothing. |
| What does it cost? | The time your component functions take to run. | The time the browser takes to change the page and draw it again. |
| What shows it here? | The counters, and React DevTools' Highlight updates. | watchDom.js, and the browser's Paint flashing. |
When a render costs something
A render costs the time your component takes to run. For most components that's a few microseconds, and a few extra renders don't matter. It matters when two things meet: a component that is slow to render, and a render that happens often, like on every key. Here the product list is slow on purpose: lines 5 to 8 of the SlowProductList.jsx tab wait 100 milliseconds in a loop, like a list with a lot to draw. Type Happy birthday quickly:
import { useState } from "react";import RenderCount from "./RenderCount.jsx";import SlowProductList from "./SlowProductList.jsx"; export default function App() { const [note, setNote] = useState(""); return ( <div className="box"> <input placeholder="Gift note" value={note} onChange={(event) => setNote(event.target.value)} /> <p>Note: {note} <RenderCount /></p> <SlowProductList /> </div> );}The letters fall behind your fingers and arrive in jumps. Line 6 keeps the note in App, and line 12 renders SlowProductList as a child, so every key renders the list, and the list's counter climbs with every letter. The box can't show your key until the render ends, 100 ms later, and 200 ms in development, where StrictMode renders every component twice.
A screen that shows 60 frames a second draws a new frame about every 16 milliseconds. A render that takes longer holds the screen for several frames, and that's when people feel it. So before you fix a render, measure it: performance.now() around the slow part (the Measuring performance lesson shows how), or the React DevTools Profiler, which the Profiling React apps lesson covers. A render that takes a millisecond can run on every key without anyone noticing.
There are a few ways to fix a slow render like this one. The simplest is in the practical section: move the state so the slow part doesn't render at all. Another is useDeferredValue, which lets the slow part lag behind while the box stays fast.
See renders with React DevTools
The counters in this lesson are a stand-in for a tool you can use on any React app: React Developer Tools, a free browser extension from the React team for Chrome, Firefox, and Edge. It works on your own app, like the one from Setting up a React project. To see renders:
- Install React Developer Tools and open your app in development.
- Open the browser's developer tools and choose the Components tab.
- Click the gear icon for its settings, and in General, turn on Highlight updates when components render.
- Use your app. A colored outline flashes around each component that renders.
The outlines are renders, not changes to the page, exactly like the blue flashes here. When one component renders many times in a short time, its outline's color moves from teal toward yellow, so a component that renders on every key stands out. The colors are a list in canvas.js in React's DevTools code. The Profiler tab goes further: it records renders, how long each took, and, with "Record why each component rendered while profiling" turned on in its settings, why.
Chrome's own developer tools can show what the browser draws again. Open the Rendering panel (from the three-dot menu, More tools, then Rendering) and turn on Paint flashing: areas the browser repaints flash green. Use both, and you see the two halves of this lesson: React DevTools for renders, Paint flashing for what changed on screen.
Moving state down to fix a slow page
The slow page had a simple cause: the note's state lived in App, above the slow list, so every key rendered the list. Only the text box and the note need that state. Move it down into a component of their own:
import { useState } from "react";import RenderCount from "./RenderCount.jsx";import SlowProductList from "./SlowProductList.jsx"; function GiftNote() { const [note, setNote] = useState(""); return ( <> <input placeholder="Gift note" value={note} onChange={(event) => setNote(event.target.value)} /> <p>Note: {note} <RenderCount /></p> </> );} export default function App() { return ( <div className="box"> <GiftNote /> <SlowProductList /> </div> );}GiftNote on lines 5 to 14 now keeps the note (line 6) and shows the box and the note. App on lines 16 to 23 has no state at all, so typing renders GiftNote and nothing else. Type Happy birthday again: the letters keep up, and the list's counter stays at renders: 1. No memo, no measuring, only a better place for the state.
Three more fixes come from the same idea, keeping the slow part out of the render:
- Pass content as children. A component with state that wraps other content, like
CartProvider, can take it aschildren. The children are made by the parent, so the wrapper's state changes don't render them. Exercise 4 practices it. - Read context as low as you can. If only one line of a slow component needs a context, put that line in a small component of its own. Exercise 5 practices it.
- Skip the render. When moving things isn't possible,
memoskips a component whose props didn't change, and the React Compiler does it for you. memo, useMemo and useCallback and The React Compiler come next.
- Its parent, ProductList, renders.
- Its own
starredstate changes. - A context it reads with
useContextgets a new value. - ProductList renders and passes exactly the same props as last time.
- Another ProductRow's state changes.
- Its
starredstate is set to the value it already has. - A context it doesn't read changes, and ProductList doesn't render.
- The page's Header renders.
ProductRow has its own starred state, and ProductList renders it with no memo. Sort each event: does ProductRow render again, or not?
Common mix-ups
- "A re-render means React rebuilt the page." No. It means React called your function. The page changes only where the new result differs, often in one text or nowhere.
- "If the props didn't change, the child won't render." No. A parent's render renders its children whatever their props, unless the child is wrapped in
memo. - "Every extra render is a bug to remove." No. Renders are how React works, and most are cheap. Fix the ones you can feel, after measuring.
- "Only the component that called the set function renders." No. Everything it returns renders too, all the way down.
- "Setting state always causes a render." No. Setting it to the value it already has (by
Object.is) renders nothing. - "StrictMode's double renders show a performance problem." No. They happen only in development, to catch impure components. A production build renders once.
Practice exercises
Read the counters in each result. Try a guess first, then press Edit & run and check, and use the hints if you're stuck.
You click Add one twice. What does Delivery's counter say?
import { useState } from "react";
import RenderCount from "./RenderCount.jsx";
function Header() {
return <h3 className="box">Tea shop <RenderCount /></h3>;
}
function Price({ quantity }) {
return <p className="box">Total: ${quantity * 4} <RenderCount /></p>;
}
function Delivery() {
return <p className="box">Free delivery over $20 <RenderCount /></p>;
}
export default function App() {
const [quantity, setQuantity] = useState(1);
return (
<div className="box">
<Header />
<button onClick={() => setQuantity(quantity + 1)}>Add one</button>
<Price quantity={quantity} />
<Delivery />
<RenderCount />
</div>
);
}import { useState } from "react";import RenderCount from "./RenderCount.jsx"; function Header() { return <h3 className="box">Tea shop <RenderCount /></h3>;} function Price({ quantity }) { return <p className="box">Total: ${quantity * 4} <RenderCount /></p>;} function Delivery() { return <p className="box">Free delivery over $20 <RenderCount /></p>;} export default function App() { const [quantity, setQuantity] = useState(1); return ( <div className="box"> <Header /> <button onClick={() => setQuantity(quantity + 1)}>Add one</button> <Price quantity={quantity} /> <Delivery /> <RenderCount /> </div> );}Delivery renders once at the start and once for each click, because App, its parent, renders. After two clicks it says renders: 3, though its text never changed.
You click Add one, Reset, then Reset again. What does Price's counter say? Then click it in the result.
import { useState } from "react";
import RenderCount from "./RenderCount.jsx";
function Price({ quantity }) {
return <p className="box">Total: ${quantity * 4} <RenderCount /></p>;
}
export default function App() {
const [quantity, setQuantity] = useState(1);
return (
<div className="box">
<button onClick={() => setQuantity(quantity + 1)}>Add one</button>
<button onClick={() => setQuantity(1)}>Reset</button>
<Price quantity={quantity} />
</div>
);
}import { useState } from "react";import RenderCount from "./RenderCount.jsx"; function Price({ quantity }) { return <p className="box">Total: ${quantity * 4} <RenderCount /></p>;} export default function App() { const [quantity, setQuantity] = useState(1); return ( <div className="box"> <button onClick={() => setQuantity(quantity + 1)}>Add one</button> <button onClick={() => setQuantity(1)}>Reset</button> <Price quantity={quantity} /> </div> );}The first render is 1. Add one sets 2: render 2. Reset sets 1, a new value: render 3. The second Reset sets 1 again, the same value, so React skips the render. Price stays at renders: 3.
Typing lags because every key renders the slow list. Fix it without touching the list. After your fix, what does the list's counter say after you type Hi?
import { useState } from "react";import RenderCount from "./RenderCount.jsx";import SlowProductList from "./SlowProductList.jsx"; export default function App() { const [note, setNote] = useState(""); return ( <div className="box"> <input placeholder="Gift note" value={note} onChange={(event) => setNote(event.target.value)} /> <p>Note: {note} <RenderCount /></p> <SlowProductList /> </div> );}import { useState } from "react";import RenderCount from "./RenderCount.jsx";import SlowProductList from "./SlowProductList.jsx"; function GiftNote() { const [note, setNote] = useState(""); return ( <> <input placeholder="Gift note" value={note} onChange={(event) => setNote(event.target.value)} /> <p>Note: {note} <RenderCount /></p> </> );} export default function App() { return ( <div className="box"> <GiftNote /> <SlowProductList /> </div> );}With the note's state in GiftNote, a key renders only GiftNote. App doesn't render, so SlowProductList doesn't either, and typing is fast. After typing Hi, the list still says renders: 1.
Every Add to cart renders ProductList, which shows nothing about the cart. Fix it with children. After two adds, what does ProductList's counter say?
import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const CartContext = createContext(0); function CartBadge() { const count = useContext(CartContext); return <p className="box">Cart: {count} <RenderCount /></p>;} function ProductList() { return <p className="box">Green tea, chai, mint tea <RenderCount /></p>;} function CartProvider() { const [count, setCount] = useState(0); return ( <CartContext value={count}> <button onClick={() => setCount(count + 1)}>Add to cart</button> <CartBadge /> <ProductList /> </CartContext> );} export default function App() { return <CartProvider />;}import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const CartContext = createContext(0); function CartBadge() { const count = useContext(CartContext); return <p className="box">Cart: {count} <RenderCount /></p>;} function ProductList() { return <p className="box">Green tea, chai, mint tea <RenderCount /></p>;} function CartProvider({ children }) { const [count, setCount] = useState(0); return ( <CartContext value={count}> <button onClick={() => setCount(count + 1)}>Add to cart</button> {children} </CartContext> );} export default function App() { return ( <CartProvider> <CartBadge /> <ProductList /> </CartProvider> );}Now App makes the two elements and passes them as children. When the count changes, CartProvider renders but its children are the same elements, so React skips ProductList. CartBadge still renders, because it reads the context. ProductList stays at renders: 1.
Each Add to cart takes at least 100 ms, because the slow list reads the cart for one line. Fix it. After two adds, what does the list's counter say?
import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const CartContext = createContext(0); function CartProvider({ children }) { const [count, setCount] = useState(0); return ( <CartContext value={count}> <button onClick={() => setCount(count + 1)}>Add to cart</button> {children} </CartContext> );} function SlowProductList() { const count = useContext(CartContext); const start = performance.now(); while (performance.now() - start < 100) { // Pretend this list has a lot to draw. } return ( <div className="box"> 200 teas <RenderCount /> <p>In your cart: {count}</p> </div> );} export default function App() { return ( <CartProvider> <SlowProductList /> </CartProvider> );}import { createContext, useContext, useState } from "react";import RenderCount from "./RenderCount.jsx"; const CartContext = createContext(0); function CartProvider({ children }) { const [count, setCount] = useState(0); return ( <CartContext value={count}> <button onClick={() => setCount(count + 1)}>Add to cart</button> {children} </CartContext> );} function InCart() { const count = useContext(CartContext); return <p>In your cart: {count}</p>;} function SlowProductList() { const start = performance.now(); while (performance.now() - start < 100) { // Pretend this list has a lot to draw. } return ( <div className="box"> 200 teas <RenderCount /> <InCart /> </div> );} export default function App() { return ( <CartProvider> <SlowProductList /> </CartProvider> );}Only InCart reads the cart now, so only InCart renders when it changes. The slow list stays at renders: 1, and Add to cart answers at once, while the line still shows the new count.
A sale countdown renders the whole page every second. Fix it. After your fix, what does ProductList's counter say three seconds later?
import { useEffect, useState } from "react";import RenderCount from "./RenderCount.jsx"; function ProductList() { return <p className="box">Green tea, chai, mint tea <RenderCount /></p>;} export default function App() { const [seconds, setSeconds] = useState(0); useEffect(() => { const id = setInterval(() => setSeconds((value) => value + 1), 1000); return () => clearInterval(id); }, []); return ( <div className="box"> <p>Sale ends in {60 - seconds} seconds</p> <ProductList /> </div> );}import { useEffect, useState } from "react";import RenderCount from "./RenderCount.jsx"; function ProductList() { return <p className="box">Green tea, chai, mint tea <RenderCount /></p>;} function SaleClock() { const [seconds, setSeconds] = useState(0); useEffect(() => { const id = setInterval(() => setSeconds((value) => value + 1), 1000); return () => clearInterval(id); }, []); return <p>Sale ends in {60 - seconds} seconds</p>;} export default function App() { return ( <div className="box"> <SaleClock /> <ProductList /> </div> );}The timer now changes SaleClock's state, so each second renders SaleClock alone. App doesn't render, and ProductList stays at renders: 1 however long you wait.
Check your understanding
For each question, find where the state lives, then follow the render down from there. Remember that a render and a change to the page are two different things.
Question 1 of 7Which of these does NOT make a component render again?
Choose an answer to see the explanation.
Question 2 of 7You click Add one three times. Counting the first render, how many times has GiftNote rendered?
Read the code, then predictimport { useState } from "react"; function GiftNote({ text }) { console.log("GiftNote renders"); return <p>{text}</p>; } export default function App() { const [count, setCount] = useState(0); return ( <> <button onClick={() => setCount(count + 1)}>Add one</button> <GiftNote text="Happy birthday" /> </> ); }Choose an answer to see the explanation.
Question 3 of 7The theme is already "light", and you click Light. What does React do?
Read the code, then predictJSXconst [theme, setTheme] = useState("light"); <button onClick={() => setTheme("light")}>Light</button>Choose an answer to see the explanation.
Question 4 of 7In the example with Header, Price, and Delivery, you click Add one. Four components render. How many things does React change on the page?
Choose an answer to see the explanation.
Question 5 of 7Why doesn't ProductCard render when you press Add to cart?
Choose an answer to see the explanation.
Question 6 of 7When is a re-render worth fixing?
Choose an answer to see the explanation.
Question 7 of 7What does React DevTools' "Highlight updates when components render" show?
Choose an answer to see the explanation.
Key takeaways
- A component renders again when its own state changes, when its parent renders, or when a context it reads changes.
- A render goes down from the component that changed: everything it returns renders, even with the same props, unless it's wrapped in
memo. - Setting state to the value it already has renders nothing. Children passed in from a parent that didn't render are skipped.
- A render calls your function; the commit changes the page only where the result differs. Many renders can end in one change, or none.
- Most renders are cheap. Measure, find the slow ones that run often, and fix them first by moving state down, passing children, or reading context lower.
- React DevTools' Highlight updates outlines renders; Chrome's Paint flashing shows what was drawn again.
Remember the one-liner.
State, parent, or context makes a component render; the page changes only where the result differs.
Coming next: memo, useMemo and useCallback. It shows how to skip a render whose inputs didn't change, and why that often doesn't help.