Balance performance and readability with React.memo, inline functions, and React.useCallback

Photo by Photoholgic on Unsplash

When should I replace an inline function with React.useCallback? What part does React.memo play? Should I heed the react/jsx-no-bind eslint rule? Here’s how I found the answers to these questions.

Setting up a simple test

I created a Vite-React application to test React rendering performance. The Card component contains the MagicNumber component. Each component writes to console.log when it renders.

function MagicNumber({ num }) {
console.log("MagicNumber rendering");
return <p>Oh, ho ho, it's magic: {num}</p>
}

function Card() {
console.log("Card rendering");
return <MagicNumber num={99} />
}

export default function App() {
return (
<main>
<Card />
</main>
);
}

When I ran the application with the production build of React 18.2, the console reported:

Card rendering
MagicNumber rendering

Next I needed a way to force the Card component to re-render. I added a button to it that updates a local state variable.

function Card() {
const [_, setState] = useState(false);

console.log("Card rendering");
return (
<>
<MagicNumber num={99} />
<button onClick={() => setState((s) => !s)}>Render again</button>
</>
);
}

Here’s what it looked like.

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering

Wait, that’s not what I wanted. Why is MagicNumber rendering every time Card renders, even though its prop value, 99, never changed? This is where React.memo comes into play.

Caching rendered components with React.memo

I wrapped the MagicNumber component with React.memo.

const MagicNumber = memo(function ({ num }) {
console.log("MagicNumber rendering");
return <p>Oh, ho ho, it's magic: {num}</p>;
});

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
Card rendering
Card rendering

React rendered MagicNumber once, and then React.memo cached the rendered component with its prop value, 99. If MagicNumber’s prop value were to change, then React would render it again. But only once for each unique prop value.

React.memo is a hash table for rendered components. The keys are the components’ props, and the values are the rendered components.

The test was working the way I expected. I was ready to compare inline functions with React.useCallback.

Setting prop values to inline functions

I added another prop to MagicNumber: a click handler.

const MagicNumber = memo(function ({ num, onClick }) {
^^^^^^^
...
return (
<p>Oh, ho ho, it's magic: <span onClick={onClick}>{num}</span></p>
^^^^^^^^^^^^^^^^^
...
});

function Card() {
...
return (
<>
<MagicNumber num={99} onClick={() => console.log("Click!")} />
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
}

Look at the Card component. I set the value of the onClick prop to an inline function, () => console.log(“Click!”).

I like inline functions as prop values because they’re easy to read. I can see what the function does without jumping to a definition somewhere else in the code. It’s locality of reference for my human brain.

Inline functions can cause rendering problems, though. When I reloaded the page and clicked the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering

Wait, why did React.memo stop caching my rendered MagicNumber component? The answer lies in what JavaScript considers “equal”.

Referential equality in JavaScript

Referential equality in JavaScript is tested with the === operator. The three equal signs mean, “Do these two things occupy the same spot in memory?” It is JavaScript’s strictest form of equality.

Primitive types in JavaScript are always referentially equal.

99 === 99            // true
true === true // true
"magic" === "magic" // true

But objects, arrays, and functions are never referentially equal.

{} === {}                // false
{ a: 99 } === { a: 99 } // false
[] === [] // false
[1, 2, 3] === [1, 2, 3] // false
() => {} === () => {} // false
() => 99 === () => 99 // false

This is why React.memo stopped working when I added the inline function prop value. Here’s how it handled MagicNumber’s first and second renders.

First render:

  1. Received a request to render a component with props (99, () => console.log(“Click!”)).
  2. Nothing in the hash table yet, so render the component and cache it.

Second render:

  1. Received a request to render a component with props (99, () => console.log(“Click!”)).
  2. Found an entry in the hash table. Compare the props to see if it matches.
  3. Does 99 === 99? Yes.
  4. Does () => console.log(“Click!”) === () => console.log(“Click!”)? No. See JavaScript referential equality for functions.
  5. Render the component (again) and cache it (again).

Yikes. I was re-rendering MagicNumber every time, and React.memo was storing every duplicate render.

Inline functions and React.memo can both cause performance or memory problems when they’re misused.

Note that declaring MagicNumbers’s prop values as local variables doesn’t solve the problem.

function Card() {
...
const num = 99;
const fn = () => console.log("Click!");
...
return (
<>
<MagicNumber num={num} onClick={fn} />
^^^ ^^
...
}

On the first render, fn gets declared and assigned to a function () => console.log(“Click!”). On the second render, fn gets declared (again) and assigned to a function (again). It happens to be a function with an identical implementation, but it is a different function. Referential equality returns false.

I needed a way to create a referentially equal declaration of the click handler function. This is where React.useCallback comes into play.

Creating referentially equal functions with React.useCallback

I replaced the inline function with React.useCallback.

function Card() {
...
const handleClick = useCallback(() => {
console.log("Click!");
}, []);
...
return (
<>
<MagicNumber num={99} onClick={handleClick} />
^^^^^^^^^^^
...
}

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
Card rendering
Card rendering

I lost the readability of the inline function when I replaced it with React.useCallback, but the application was rendering as expected.

React.useCallback creates a function that’s referentially equal across multiple renders. It’s only useful when passed to a memoized component.

Now I could answer those original questions I asked.

Summary

Here are the guidelines I use for React.memo, inline functions, and React.useCallback.

  • Use React.memo for components that are both 1) costly to render and 2) frequently rendered. For smaller components the performance improvement is only a few fractions of a millisecond. Measure first.
  • Use inline functions for components that aren’t memoized. It improves readability and allows performance problems to appear before you fix them. That’s a much better strategy than fixing performance problems before they appear. Because they might never appear.
  • Use inline functions for all DOM components, e.g. <button onClick={() => {}}>…</button>. You can’t memoize DOM components, so inline functions have no drawbacks.
  • Use React.useCallback for memoized components. You lose the readability of inline functions, but React.memo works as expected.
  • The react/jsx-no-bind rule is not part of the react/recommended configuration for good reason: it doesn’t allow you to make the judgements and exceptions described in the first two bullets. If you do use it, then be sure to configure it to ignore DOM components and ignore refs.

Additional Sources


Balance performance and readability with React.memo, inline functions, and React.useCallback was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Seth Livingston

Photo by Photoholgic on Unsplash

When should I replace an inline function with React.useCallback? What part does React.memo play? Should I heed the react/jsx-no-bind eslint rule? Here’s how I found the answers to these questions.

Setting up a simple test

I created a Vite-React application to test React rendering performance. The Card component contains the MagicNumber component. Each component writes to console.log when it renders.

function MagicNumber({ num }) {
console.log("MagicNumber rendering");
return <p>Oh, ho ho, it's magic: {num}</p>
}

function Card() {
console.log("Card rendering");
return <MagicNumber num={99} />
}

export default function App() {
return (
<main>
<Card />
</main>
);
}

When I ran the application with the production build of React 18.2, the console reported:

Card rendering
MagicNumber rendering

Next I needed a way to force the Card component to re-render. I added a button to it that updates a local state variable.

function Card() {
const [_, setState] = useState(false);

console.log("Card rendering");
return (
<>
<MagicNumber num={99} />
<button onClick={() => setState((s) => !s)}>Render again</button>
</>
);
}

Here’s what it looked like.

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering

Wait, that’s not what I wanted. Why is MagicNumber rendering every time Card renders, even though its prop value, 99, never changed? This is where React.memo comes into play.

Caching rendered components with React.memo

I wrapped the MagicNumber component with React.memo.

const MagicNumber = memo(function ({ num }) {
console.log("MagicNumber rendering");
return <p>Oh, ho ho, it's magic: {num}</p>;
});

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
Card rendering
Card rendering

React rendered MagicNumber once, and then React.memo cached the rendered component with its prop value, 99. If MagicNumber's prop value were to change, then React would render it again. But only once for each unique prop value.

React.memo is a hash table for rendered components. The keys are the components’ props, and the values are the rendered components.

The test was working the way I expected. I was ready to compare inline functions with React.useCallback.

Setting prop values to inline functions

I added another prop to MagicNumber: a click handler.

const MagicNumber = memo(function ({ num, onClick }) {
^^^^^^^
...
return (
<p>Oh, ho ho, it's magic: <span onClick={onClick}>{num}</span></p>
^^^^^^^^^^^^^^^^^
...
});

function Card() {
...
return (
<>
<MagicNumber num={99} onClick={() => console.log("Click!")} />
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
}

Look at the Card component. I set the value of the onClick prop to an inline function, () => console.log(“Click!”).

I like inline functions as prop values because they’re easy to read. I can see what the function does without jumping to a definition somewhere else in the code. It’s locality of reference for my human brain.

Inline functions can cause rendering problems, though. When I reloaded the page and clicked the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering
Card rendering
MagicNumber rendering

Wait, why did React.memo stop caching my rendered MagicNumber component? The answer lies in what JavaScript considers “equal”.

Referential equality in JavaScript

Referential equality in JavaScript is tested with the === operator. The three equal signs mean, “Do these two things occupy the same spot in memory?” It is JavaScript’s strictest form of equality.

Primitive types in JavaScript are always referentially equal.

99 === 99            // true
true === true // true
"magic" === "magic" // true

But objects, arrays, and functions are never referentially equal.

{} === {}                // false
{ a: 99 } === { a: 99 } // false
[] === [] // false
[1, 2, 3] === [1, 2, 3] // false
() => {} === () => {} // false
() => 99 === () => 99 // false

This is why React.memo stopped working when I added the inline function prop value. Here’s how it handled MagicNumber’s first and second renders.

First render:

  1. Received a request to render a component with props (99, () => console.log("Click!")).
  2. Nothing in the hash table yet, so render the component and cache it.

Second render:

  1. Received a request to render a component with props (99, () => console.log("Click!")).
  2. Found an entry in the hash table. Compare the props to see if it matches.
  3. Does 99 === 99? Yes.
  4. Does () => console.log("Click!") === () => console.log("Click!")? No. See JavaScript referential equality for functions.
  5. Render the component (again) and cache it (again).

Yikes. I was re-rendering MagicNumber every time, and React.memo was storing every duplicate render.

Inline functions and React.memo can both cause performance or memory problems when they’re misused.

Note that declaring MagicNumbers's prop values as local variables doesn’t solve the problem.

function Card() {
...
const num = 99;
const fn = () => console.log("Click!");
...
return (
<>
<MagicNumber num={num} onClick={fn} />
^^^ ^^
...
}

On the first render, fn gets declared and assigned to a function () => console.log("Click!"). On the second render, fn gets declared (again) and assigned to a function (again). It happens to be a function with an identical implementation, but it is a different function. Referential equality returns false.

I needed a way to create a referentially equal declaration of the click handler function. This is where React.useCallback comes into play.

Creating referentially equal functions with React.useCallback

I replaced the inline function with React.useCallback.

function Card() {
...
const handleClick = useCallback(() => {
console.log("Click!");
}, []);
...
return (
<>
<MagicNumber num={99} onClick={handleClick} />
^^^^^^^^^^^
...
}

When I reloaded the page and pushed the button three times, the console reported:

Card rendering
MagicNumber rendering
Card rendering
Card rendering
Card rendering

I lost the readability of the inline function when I replaced it with React.useCallback, but the application was rendering as expected.

React.useCallback creates a function that’s referentially equal across multiple renders. It’s only useful when passed to a memoized component.

Now I could answer those original questions I asked.

Summary

Here are the guidelines I use for React.memo, inline functions, and React.useCallback.

  • Use React.memo for components that are both 1) costly to render and 2) frequently rendered. For smaller components the performance improvement is only a few fractions of a millisecond. Measure first.
  • Use inline functions for components that aren’t memoized. It improves readability and allows performance problems to appear before you fix them. That’s a much better strategy than fixing performance problems before they appear. Because they might never appear.
  • Use inline functions for all DOM components, e.g. <button onClick={() => {}}>...</button>. You can’t memoize DOM components, so inline functions have no drawbacks.
  • Use React.useCallback for memoized components. You lose the readability of inline functions, but React.memo works as expected.
  • The react/jsx-no-bind rule is not part of the react/recommended configuration for good reason: it doesn’t allow you to make the judgements and exceptions described in the first two bullets. If you do use it, then be sure to configure it to ignore DOM components and ignore refs.

Additional Sources


Balance performance and readability with React.memo, inline functions, and React.useCallback was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Seth Livingston


Print Share Comment Cite Upload Translate Updates
APA

Seth Livingston | Sciencx (2023-03-13T11:56:21+00:00) Balance performance and readability with React.memo, inline functions, and React.useCallback. Retrieved from https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/

MLA
" » Balance performance and readability with React.memo, inline functions, and React.useCallback." Seth Livingston | Sciencx - Monday March 13, 2023, https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/
HARVARD
Seth Livingston | Sciencx Monday March 13, 2023 » Balance performance and readability with React.memo, inline functions, and React.useCallback., viewed ,<https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/>
VANCOUVER
Seth Livingston | Sciencx - » Balance performance and readability with React.memo, inline functions, and React.useCallback. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/
CHICAGO
" » Balance performance and readability with React.memo, inline functions, and React.useCallback." Seth Livingston | Sciencx - Accessed . https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/
IEEE
" » Balance performance and readability with React.memo, inline functions, and React.useCallback." Seth Livingston | Sciencx [Online]. Available: https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/. [Accessed: ]
rf:citation
» Balance performance and readability with React.memo, inline functions, and React.useCallback | Seth Livingston | Sciencx | https://www.scien.cx/2023/03/13/balance-performance-and-readability-with-react-memo-inline-functions-and-react-usecallback/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.