Grokking React Advanced
Vote

0% completed

​

Memoization Using React.memo

A Child That Re-renders for No Reason

The Same Child with memo

How memo Decides

What memo Cannot Skip

A Custom Comparison

When memo Is Worth Adding

When a component re-renders, React by default re-renders every component it returns, and every component those return, down the whole subtree. It does this even when a child receives exactly the same props as last time. The memo function changes that for one component: it makes React compare the new props with the previous ones and, if they are all equal, reuse the component's last output instead of calling it again. A component wrapped this way is called a memoized component.

memo is imported from react and is often written React.memo in existing code. Both names refer to the same function.

import { memo } from 'react'; const Greeting = memo(function Greeting({ name }) { return <h3>Hello, {name}!</h3>; });

memo takes a component and returns a new component. You use Greeting exactly as before, and it accepts the same props.

A Child That Re-renders for No Reason

Here a parent holds a counter, and its child always receives the same name:

import { useState } from 'react'; function Child({ name }) { console.log('Child component re-rendered!'); return <h3>Hello, {name}!</h3>; } export default function Parent() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count + 1)}>Increase Count ({count})</button> <Child name="John" /> </div> ); }

We rendered Parent and clicked the button three times, counting the messages Child logged:

without memo: mount logs 1 | 3 clicks logs 3

Each click changes count, so React re-renders Parent. Parent returns <Child name="John" /> again, and by default React renders Child again as well.

The child is not created anew: it is the same component instance, and any state it holds survives. In our test, a child with its own counter at 2 still showed 2 after the parent re-rendered. React only calls the child's function again to get its output.

In development you may see each message twice. Vite's starter project wraps the app in StrictMode, which renders every component twice in development to help find impure code. Production builds render once.

The Same Child with memo

import { memo, useState } from 'react'; const Child = memo(function Child({ name }) { console.log('Child component re-rendered!'); return <h3>Hello, {name}!</h3>; });

Parent is unchanged. The same three clicks now give:

with memo: mount logs 1 | 3 clicks logs 0

On each click, React re-renders Parent, which returns <Child name="John" /> as before. This time it compares each new prop with the previous one using Object.is. The only prop, name, is "John" both times, so React skips calling Child and keeps its previous output on the screen.

The component is passed to memo as a named function, function Child. An anonymous arrow function, as in memo(({ name }) => ...), works the same way, but the component then has no name, and React Developer Tools and error messages show it as anonymous.

How memo Decides

When the parent re-renders, memo compares each prop with Object.is and either skips the child or renders it.
When the parent re-renders, memo compares each prop with Object.is and either skips the child or renders it.

The diagram shows the decision React makes for a memoized child each time its parent re-renders. Notice that the comparison is shallow: React compares each prop once, by value for strings and numbers and by identity for objects, arrays and functions. It never looks inside an object.

Because of this shallow comparison, memo often stops working without any warning. We kept the three clicks and changed only how Parent passes the props:

const john = { name: 'John' }; // created once, outside the component <Greeting user={john} /> // B1: same object every render <Greeting user={{ name: 'John' }} /> // B2: new object every render <Greeting user={john} onWave={() => {}} /> // B3: new function every render
B1 stable object prop | Greeting renders: mount + 3 clicks = 1 B2 new object literal each render | Greeting renders: mount + 3 clicks = 4 B3 new inline function each render | Greeting renders: mount + 3 clicks = 4

In B2, { name: 'John' } is a new object on every render of Parent. Its contents are equal, but Object.is compares identity, so the prop counts as changed and memo renders the child every time. B3 fails for the same reason: an arrow function written inside Parent is a new function on every render. A computed number such as amount={10 * 2} is fine, because 20 equals 20; our run of that case rendered the child once.

To keep an object or a function prop stable, create it outside the component when it does not depend on props or state, as john is. When it does depend on them, use useMemo for objects and arrays and useCallback for functions. Those two hooks are the subjects of the last two lessons of this chapter.

What memo Cannot Skip

memo only looks at props. A memoized component still re-renders when its own state changes, and when a context it reads changes:

const LangContext = createContext('en'); const Label = memo(function Label() { const lang = useContext(LangContext); const [likes, setLikes] = useState(0); return <button onClick={() => setLikes(likes + 1)}>{lang} likes {likes}</button>; });
after own state change, renders: 2 en likes 1 after context change, renders: 3 fr likes 1

Both renders are needed, because both changed what Label shows. memo only prevents renders caused by a parent re-rendering with equal props.

A Custom Comparison

memo accepts an optional second argument, a function that receives the previous and the next props and returns true when they should count as equal:

const ByName = memo( function ByName({ user }) { return <h3>{user.name}</h3>; }, (prevProps, nextProps) => prevProps.user.name === nextProps.user.name );

With a new user object on every click, this component rendered once over the mount and three clicks. Use this sparingly. If the function returns true while some prop really changed, for example a callback that now reads newer state, the component keeps showing or using old values. A comparison that walks deep into large objects can also cost more than the render it avoids.

When memo Is Worth Adding

memo helps when both of these are true: the component often re-renders with exactly the same props, and rendering it takes noticeable time, such as a chart or a list of hundreds of rows. For a small component like Child, a render costs very little, and the comparison saves almost nothing.

memo also does nothing when a prop is new on every render, so before adding it, check what the parent passes. React's own guidance is that if there is no lag you can notice, memo is not needed. When React Compiler is enabled, a compiled parent reuses a child's element while the child's props are unchanged, so React skips the child and you usually do not need to write memo yourself. The lesson "Automatic Memoization with React Compiler" shows how this works.

Reading Progress

0%


Vote for new content

On This Page

A Child That Re-renders for No Reason

The Same Child with memo

How memo Decides

What memo Cannot Skip

A Custom Comparison

When memo Is Worth Adding