Browse Source

[Beta] useCallback (#5061)

main
dan 2 years ago
committed by GitHub
parent
commit
6001def102
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      beta/src/components/MDX/Sandpack/SandpackRoot.tsx
  2. 792
      beta/src/content/apis/react/useCallback.md
  3. 136
      beta/src/content/apis/react/useMemo.md
  4. 3
      beta/src/sidebarReference.json

4
beta/src/components/MDX/Sandpack/SandpackRoot.tsx

@ -58,6 +58,10 @@ h6 {
font-size: 12px;
}
code {
font-size: 1.2em;
}
ul {
padding-left: 20px;
}

792
beta/src/content/apis/react/useCallback.md

@ -2,19 +2,797 @@
title: useCallback
---
<Wip>
<Intro>
This section is incomplete, please see the old docs for [useCallback.](https://reactjs.org/docs/hooks-reference.html#usecallback)
`useCallback` is a React Hook that lets you reuse a function definition from a past render.
</Wip>
```js
const memoizedFn = useCallback(fn, dependencies)
```
</Intro>
<InlineToc />
---
## Usage {/*usage*/}
### Skipping re-rendering of components {/*skipping-re-rendering-of-components*/}
By default, when a component re-renders, React re-renders all of its children recursively. This is fine for components that don't require much calculation to re-render. Components higher up the tree or slower components can opt into *skipping re-renders when their props are the same* by wrapping themselves in [`memo`](/apis/react/memo):
```js {1,7}
import { memo } from 'react';
function ShippingForm({ onSubmit }) {
// ...
}
export default memo(ShippingForm);
```
Let's say the `ProductPage` component passes a `handleSubmit` function to that `ShippingForm` component:
```js {2-7,11}
function ProductPage({ product, referrerId, theme }) {
function handleSubmit(orderDetails) {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
```
Suppose the user toggles the theme, and the `ProductPage` receives a different `theme` prop. You might expect that `ShippingForm` will skip re-rendering because its props are not affected by the `theme`.
Unfortunately, even if only the `theme` changes, the `ShippingForm` will have to re-render. Its [`memo`](/apis/react/memo) optimization will not work because the value of the `onSubmit` prop will be *different on every re-render.* In JavaScript, `function() {}` and `() => {}` function declarations always create a *different* function, similar to how `{}` creates a *different* object. By passing `handleSubmit`, you always pass a *different* function to `ShippingForm`.
**To prevent `handleSubmit` from changing on every re-render, wrap its definition into the `useCallback` Hook:**
```js [[3, 4, "handleSubmit"], [2, 9, "[product, referrerId]"]]
import { useCallback } from 'react';
function ProductPage({ product, referrerId, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]);
return (
<div className={theme}>
<ThemeSwitcher value={theme} onChange={setTheme} />
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
```
<Intro>
You need to pass two things to `useCallback`:
1. A function that you want to pass down to the child component.
2. A <CodeStep step={2}>list of dependencies</CodeStep> including every value within your component that's used inside your function.
On the initial render, the <CodeStep step={3}>returned function</CodeStep> you'll get from `useCallback` will be the function you passed.
On every next render, React will compare the <CodeStep step={2}>dependencies</CodeStep> with the dependencies you passed during the last render. If none of the dependencies have changed (compared with [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)), `useCallback` will return the function you passed on the *last* render. Otherwise, React will return the function you passed on *this* render.
In other words, `useCallback` will cache your function, and return it on re-renders until the dependencies change. If both `product` and `referrerId` are the same as before, the `ProductPage` will pass the *same* `handleSubmit` function to the `ShippingForm`. The `ShippingForm` is wrapped in [`memo`](/apis/react/memo), so it will skip a render with same props.
<Note>
**You should only rely on `useCallback` as a performance optimization.** If your code doesn't work without it, find the underlying problem and fix it first. Then you may add `useCallback` to improve performance.
</Note>
<DeepDive title="How is useCallback related to useMemo?">
You will often see [`useMemo`](/apis/react/useMemo) alongside `useCallback`. They are both useful when you're trying to optimize a child component. They let you [memoize](https://en.wikipedia.org/wiki/Memoization) (or, in other words, cache) something you're passing down:
```js {4-6,8-13,17}
import { useMemo, useCallback } from 'react';
function ProductPage({ product, referrerId }) {
const requirements = useMemo(() => { // Calls your function and caches its result
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Caches your function itself
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
```
The difference is in *what* they're letting you cache:
* **[`useMemo`](/apis/react/useMemo) caches the *result* of calling your function.** In this example, it caches the result of calling `computeRequirements(product)` so that it doesn't change unless `product` has changed. This lets you pass the `requirements` object down without unnecessarily re-rendering `ShippingForm`. When necessary, React will call the function you've passed during rendering to calculate the result.
* **`useCallback` caches *the function itself.*** Unlike `useMemo`, it does not call the function you provide. Instead, it caches the function you provided so that `handleSubmit` *itself* doesn't change unless `product` or `referrerId` has changed. This lets you pass the `handleSubmit` function down without unnecessarily re-rendering `ShippingForm`. Your code won't be called until the user submits the form.
If you're already familiar with [`useMemo`,](/apis/react/useMemo) you might find it helpful to think of `useCallback` as this:
```js
const memoizedCallback = useCallback(callback, [...dependencies])
// Simplified implementation (inside React)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
```
</Intro>
[Read more about the difference between `useMemo` and `useCallback`.](/apis/react/useMemo#memoizing-a-function)
<InlineToc />
</DeepDive>
<Recipes titleText="The difference between useCallback and declaring a function directly" titleId="examples-rerendering">
#### Skipping re-rendering with `useCallback` and `memo` {/*skipping-re-rendering-with-usecallback-and-memo*/}
In this example, the `ShippingForm` component is **artificially slowed down** so that you can see what happens when a React component you're rendering is genuinely slow. Try incrementing the counter and toggling the theme.
When you increment the counter, the `ShippingForm` re-renders. Since its rendering is artificially slowed down, the interaction feels slow. Then try toggling the theme. You'll notice that toggling the theme is fast because the slowed-down `ShippingForm` component skips re-rendering. It is able to skip re-rendering because it's wrapped in [`memo`](/apis/react/memo) *and* the props passed to it are the same as during the last render. Specifically, the `handleSubmit` function does not change between the re-renders thanks to `useCallback`. Its dependencies (`product` and `referrerId`) have not changed, so `useCallback` returns a cached function.
<Sandpack>
```js App.js
import { useState } from 'react';
import ProductPage from './ProductPage.js';
const product = {
id: 123,
name: 'A hot air balloon'
};
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
referrerId="wizard_of_oz"
product={product}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
function sendData(product, orderDetails, referrerId) {
console.log('POST /products/' + product.id + '/buy?ref=' + referrerId);
console.log(orderDetails);
}
```
```js ProductPage.js active
import { useCallback } from 'react';
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ product, referrerId, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]);
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
```
```js ShippingForm.js
import { memo, useState } from 'react';
function ShippingForm({ onSubmit }) {
const [count, setCount] = useState(1);
console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// Do nothing for 500 ms to emulate extremely slow code
}
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const orderDetails = {
...Object.fromEntries(formData),
count
};
onSubmit(orderDetails);
}
return (
<form onSubmit={handleSubmit}>
<p><b>Note: <code>ShippingForm</code> is artificially slowed down!</b></p>
<label>
Number of items:
<button type="button" onClick={() => setCount(count - 1)}>–</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</label>
<label>
Street:
<input name="street" />
</label>
<label>
City:
<input name="city" />
</label>
<label>
Postal code:
<input name="zipCode" />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default memo(ShippingForm);
```
```css
label {
display: block; margin-top: 10px;
}
input {
margin-left: 5px;
}
button[type="button"] {
margin: 5px;
}
.dark {
background-color: black;
color: white;
}
.light {
background-color: white;
color: black;
}
```
</Sandpack>
<Solution />
#### Always re-rendering a component {/*always-re-rendering-a-component*/}
This example is the same as the previous one, but it doesn't have a `useCallback` call.
Try switching the theme in this example. It should feel much slower than the first one!
When you toggle the theme, the `App` component re-renders. The `ProductPage` component re-renders too and creates a new `handleSubmit` function. Creating a function by itself is not a problem, but it passes this function down to the **artificially slowed down** `ShippingForm` component. Although `ShippingForm` is wrapped in [`memo`,](/apis/react/memo) it can't skip re-rendering because its `onSubmit` prop is different from the last time. Toggling the theme feels slow even though `ShippingForm` doesn't use `theme`.
<Sandpack>
```js App.js
import { useState } from 'react';
import ProductPage from './ProductPage.js';
const product = {
id: 123,
name: 'A hot air balloon'
};
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
referrerId="wizard_of_oz"
product={product}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
function sendData(product, orderDetails, referrerId) {
console.log('POST /products/' + product.id + '/buy?ref=' + referrerId);
console.log(orderDetails);
}
```
```js ProductPage.js active
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ product, referrerId, theme }) {
function handleSubmit(orderDetails) {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
```
```js ShippingForm.js
import { memo, useState } from 'react';
function ShippingForm({ onSubmit }) {
const [count, setCount] = useState(1);
console.log('[ARTIFICIALLY SLOW] Rendering <ShippingForm />');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// Do nothing for 500 ms to emulate extremely slow code
}
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const orderDetails = {
...Object.fromEntries(formData),
count
};
onSubmit(orderDetails);
}
return (
<form onSubmit={handleSubmit}>
<p><b>Note: <code>ShippingForm</code> is artificially slowed down!</b></p>
<label>
Number of items:
<button type="button" onClick={() => setCount(count - 1)}>–</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</label>
<label>
Street:
<input name="street" />
</label>
<label>
City:
<input name="city" />
</label>
<label>
Postal code:
<input name="zipCode" />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default memo(ShippingForm);
```
```css
label {
display: block; margin-top: 10px;
}
input {
margin-left: 5px;
}
button[type="button"] {
margin: 5px;
}
.dark {
background-color: black;
color: white;
}
.light {
background-color: white;
color: black;
}
```
</Sandpack>
However, here is the same code **with the artificial slowdown removed:**
<Sandpack>
```js App.js
import { useState } from 'react';
import ProductPage from './ProductPage.js';
const product = {
id: 123,
name: 'A hot air balloon'
};
export default function App() {
const [isDark, setIsDark] = useState(false);
return (
<>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<ProductPage
referrerId="wizard_of_oz"
product={product}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
function sendData(product, orderDetails, referrerId) {
console.log('POST /products/' + product.id + '/buy?ref=' + referrerId);
console.log(orderDetails);
}
```
```js ProductPage.js active
import ShippingForm from './ShippingForm.js';
export default function ProductPage({ product, referrerId, theme }) {
function handleSubmit(orderDetails) {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function post(url, data) {
// Imagine this sends a request...
console.log('POST /' + url);
console.log(data);
}
```
```js ShippingForm.js
import { memo, useState } from 'react';
function ShippingForm({ onSubmit }) {
const [count, setCount] = useState(1);
console.log('Rendering <ShippingForm />');
function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const orderDetails = {
...Object.fromEntries(formData),
count
};
onSubmit(orderDetails);
}
return (
<form onSubmit={handleSubmit}>
<label>
Number of items:
<button type="button" onClick={() => setCount(count - 1)}>–</button>
{count}
<button type="button" onClick={() => setCount(count + 1)}>+</button>
</label>
<label>
Street:
<input name="street" />
</label>
<label>
City:
<input name="city" />
</label>
<label>
Postal code:
<input name="zipCode" />
</label>
<button type="submit">Submit</button>
</form>
);
}
export default memo(ShippingForm);
```
```css
label {
display: block; margin-top: 10px;
}
input {
margin-left: 5px;
}
button[type="button"] {
margin: 5px;
}
.dark {
background-color: black;
color: white;
}
.light {
background-color: white;
color: black;
}
```
</Sandpack>
Quite often, code without memoization works fine. If your interactions are fast enough, you don't need memoization.
Keep in mind that you need to run React in production mode, disable [React Developer Tools](/learn/react-developer-tools), and use devices similar to the ones your app's users have in order to get a realistic sense of what's actually slowing down your app.
<Solution />
</Recipes>
---
### Updating state from a memoized callback {/*updating-state-from-a-memoized-callback*/}
Sometimes, you might need to update state based on previous state from a memoized callback.
This `handleAddTodo` function specifies `todos` as a dependency because it computes the next todos from it:
```js {6,7}
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
```
You'll usually want your memoized functions to have as few dependencies as possible. **When you read some state only to calculate the next state, you can remove that dependency by passing an [updater function](/apis/react/useState#updating-state-based-on-the-previous-state) instead:**
```js {6,7}
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // No need for the todos dependency
// ...
```
Here, instead of making `todos` a dependency of your function and reading it there, you pass an instruction about *how* to update the state (`todos => [...todos, newTodo]`) to React. [Read more about updater functions.](/apis/react/useState#updating-state-based-on-the-previous-state)
### Preventing an Effect from firing too often {/*preventing-an-effect-from-firing-too-often*/}
Sometimes, you might want to call a function from inside an [Effect:](/learn/synchronizing-with-effects)
```js {4-9,12}
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
// ...
```
This creates a problem. [Every reactive value must be declared as a dependency of your Effect.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specified-every-reactive-value-as-a-dependency) However, if you declare `createOptions` as a dependency, it will cause your Effect to constantly reconnect to the chat room:
```js {6}
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: This dependency changes on every render
// ...
```
To solve this, you can wrap the function you need to call from an Effect into `useCallback`:
```js {4-9,16}
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Only changes when createOptions changes
// ...
```
This ensures that the `createOptions` function is the same between re-renders if the `roomId` is the same. **However, it's even better to remove the need for a function dependency** by pulling the function *inside* the Effect:
```js {5-10,16}
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
```
[Read more about removing unnecessary Effect dependencies.](/learn/removing-effect-dependencies)
---
## Reference {/*reference*/}
### `useCallback(fn, dependencies)` {/*usecallback*/}
Call `useCallback` at the top level of your component to declare a memoized callback:
```js {4,9}
import { useCallback } from 'react';
export default function ProductPage({ product, referrerId, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]);
```
[See more examples above.](#examples-rerendering)
#### Parameters {/*parameters*/}
* `fn`: The function value that you want to memoize. It can take any arguments and return any values. React will return (not call!) your function back to you during the initial render. On subsequent renders, React will return the same function again if the `dependencies` have not changed since the last render. Otherwise, it will give you the function that you have passed during the current render, and store it in case it can be reused later. React will not call the function. The function is returned to you so you can decide when and whether to call it.
* `dependencies`: The list of all reactive values referenced inside of the `fn` code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is [configured for React](/learn/editor-setup#linting), it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like `[dep1, dep2, dep3]`. React will compare each dependency with its previous value using the [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) comparison algorithm.
#### Returns {/*returns*/}
On the initial render, `useCallback` returns the `fn` function you have passed.
During subsequent renders, it will either return an already stored `fn` function from the last render (if the dependencies haven't changed), or return the `fn` function you have passed during this render.
#### Caveats {/*caveats*/}
* `useCallback` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it.
* React **will not throw away the cached function unless there is a specific reason to do that.** For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache--for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should match your expectations if you rely on `useCallback` as a performance optimization. Otherwise, a [state variable](/apis/react/useState#im-trying-to-set-state-to-a-function-but-it-gets-called-instead) or a [ref](/apis/react/useRef#avoiding-recreating-the-ref-contents) may be more appropriate.
---
## Troubleshooting {/*troubleshooting*/}
### Every time my component renders, `useCallback` returns a different function {/*every-time-my-component-renders-usecallback-returns-a-different-function*/}
Make sure you've specified the dependency array as a second argument!
If you forget the dependency array, `useCallback` will return a new function every time:
```js {7}
function ProductPage({ product, referrerId }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}); // 🔴 Returns a new function every time: no dependency array
// ...
```
This is the corrected version passing the dependency array as a second argument:
```js {7}
function ProductPage({ product, referrerId }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]); // ✅ Does not return a new function unnecessarily
// ...
```
If this doesn't help, then the problem is that at least one of your dependencies is different from the previous render. You can debug this problem by manually logging your dependencies to the console:
```js {5}
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [product, referrerId]);
console.log([product, referrerId]);
```
You can then right-click on the arrays from different re-renders in the console and select "Store as a global variable" for both of them. Assuming the first one got saved as `temp1` and the second one got saved as `temp2`, you can then use the browser console to check whether each dependency in both arrays is the same:
```js
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
```
When you find which dependency is breaking memoization, either find a way to remove it, or [memoize it as well.](/api/react/useMemo#memoizing-a-dependency-of-another-hook)

136
beta/src/content/apis/react/useMemo.md

@ -4,10 +4,10 @@ title: useMemo
<Intro>
`useMemo` is a React Hook that lets you skip recalculating a value on a re-render.
`useMemo` is a React Hook that lets you reuse a calculation from a past render.
```js
const value = useMemo(calculateValue, dependencies)
const memoizedValue = useMemo(calculateValue, dependencies)
```
</Intro>
@ -29,9 +29,9 @@ function TodoList({ todos, tab, theme }) {
}
```
**Usually, this isn't a problem because most calculations are very fast.** However, if you're filtering or transforming a large array, or doing some other expensive computation, you might want to skip doing it again unless something has changed. If both `todos` and `tab` are the same as they were during the last render, you can instruct React to reuse the `visibleTodos` you've already calculated during the last render. This technique is called *[memoization.](https://en.wikipedia.org/wiki/Memoization)*
**Usually, this isn't a problem because most calculations are very fast.** However, if you're filtering or transforming a large array, or doing some expensive computation, you might want to skip doing it again if data hasn't changed. If both `todos` and `tab` are the same as they were during the last render, you can instruct React to reuse the `visibleTodos` you've already calculated during the last render. This type of caching is called *[memoization.](https://en.wikipedia.org/wiki/Memoization)*
To declare a memoized value, wrap your calculation into a `useMemo` call at the top level of your component:
**To cache a value between re-renders, wrap its calculation in a `useMemo` call at the top level of your component:**
```js [[3, 4, "visibleTodos"], [1, 4, "() => filterTodos(todos, tab)"], [2, 4, "[todos, tab]"]]
import { useMemo } from 'react';
@ -51,9 +51,11 @@ On the initial render, the <CodeStep step={3}>value</CodeStep> you'll get from `
On every next render, React will compare the <CodeStep step={2}>dependencies</CodeStep> with the dependencies you passed during the last render. If none of the dependencies have changed (compared with [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)), `useMemo` will return the value you already calculated on the last render. Otherwise, React will re-run your calculation and return the new value.
In other words, `useMemo` will cache your function's result, and return it on re-renders until the dependencies change. If both `todos` and `tab` are the same as before, the `TodoList` won't have to recalculate `visibleTodos`.
<Note>
**You should only rely on `useMemo` as a performance optimization.** If your code doesn't work without it, find the underlying problem and fix it first. Then you may add memoization to improve performance.
**You should only rely on `useMemo` as a performance optimization.** If your code doesn't work without it, find the underlying problem and fix it first. Then you may add `useMemo` to improve performance.
</Note>
@ -150,6 +152,7 @@ export default function TodoList({ todos, theme, tab }) {
);
return (
<div className={theme}>
<p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
<ul>
{visibleTodos.map(todo => (
<li key={todo.id}>
@ -180,10 +183,9 @@ export function createTodos() {
export function filterTodos(todos, tab) {
console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// do nothing to emulate a slow computer
// Do nothing for 500 ms to emulate extremely slow code
}
return todos.filter(todo => {
@ -279,6 +281,7 @@ export default function TodoList({ todos, theme, tab }) {
return (
<div className={theme}>
<ul>
<p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p>
{visibleTodos.map(todo => (
<li key={todo.id}>
{todo.completed ?
@ -308,10 +311,9 @@ export function createTodos() {
export function filterTodos(todos, tab) {
console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// do nothing to emulate a slow computer
// Do nothing for 500 ms to emulate extremely slow code
}
return todos.filter(todo => {
@ -607,6 +609,7 @@ export default function TodoList({ todos, theme, tab }) {
);
return (
<div className={theme}>
<p><b>Note: <code>List</code> is artificially slowed down!</b></p>
<List items={visibleTodos} />
</div>
);
@ -618,10 +621,9 @@ import { memo } from 'react';
function List({ items }) {
console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// do nothing to emulate a slow computer
// Do nothing for 500 ms to emulate extremely slow code
}
return (
@ -747,6 +749,7 @@ export default function TodoList({ todos, theme, tab }) {
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
<p><b>Note: <code>List</code> is artificially slowed down!</b></p>
<List items={visibleTodos} />
</div>
);
@ -758,10 +761,9 @@ import { memo } from 'react';
function List({ items }) {
console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// do nothing to emulate a slow computer
// Do nothing for 500 ms to emulate extremely slow code
}
return (
@ -951,7 +953,7 @@ label {
</Sandpack>
Quite often, code without memoization works fine. If your interactions are fast enough, you might not need memoization.
Quite often, code without memoization works fine. If your interactions are fast enough, you don't need memoization.
Keep in mind that you need to run React in production mode, disable [React Developer Tools](/learn/react-developer-tools), and use devices similar to the ones your app's users have in order to get a realistic sense of what's actually slowing down your app.
@ -963,73 +965,49 @@ Keep in mind that you need to run React in production mode, disable [React Devel
### Memoizing a dependency of another Hook {/*memoizing-a-dependency-of-another-hook*/}
Suppose you have a calculation that depends on a `searchOptions` object:
Suppose you have a calculation that depends on an object created directly in the component body:
```js {6}
```js {2-3,7}
function Dropdown({ allItems, text }) {
const searchOptions = {
matchMode: 'whole-word',
text: text
};
const visibleItems = searchItems(allItems, searchOptions);
// ...
}
```
You try to memoize the result of calling `searchItems`:
// This object is created directly in the component body
const searchOptions = { matchMode: 'whole-word', text };
```js {6-8}
function Dropdown({ allItems, text }) {
const searchOptions = {
matchMode: 'whole-word',
text: text
};
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🔴 Memoization doesn't work: searchOptions is always new
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
```
However, this doesn't improve anything: `searchItems` would still get called on each render.
Here's why this happens. Every time that a component re-renders, all of the code directly inside the component body runs again. **The lines of code creating a new `searchOptions` object will also run on every re-render.** Since `searchOptions` is a dependency of your `useMemo` call, and it's different every time, React will consider the dependencies as being different from the last time, and will have to call your `searchItems` function again.
Depending on an object like this defeats the point of memoization. When a component re-renders, all of the code directly inside the component body runs again. **The lines of code creating the `searchOptions` object will also run on every re-render.** Since `searchOptions` is a dependency of your `useMemo` call, and it's different every time, React will know the dependencies are different from the last time, and recalculate `searchItems` every time.
When you can, it's best to fix this by moving the object creation *inside* the `useMemo` call:
To fix this, you could memoize the `searchOptions` object *itself* before passing it as a dependency:
```js {3-6,8}
```js {2-4,8}
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes
const visibleItems = useMemo(() => {
const searchOptions = {
matchMode: 'whole-word',
text: text
};
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Memoization works
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...
```
Now your calculation depends on `text` (which is a string and can't be unintentionally new).
Alternatively, you could memoize the `searchOptions` object *itself* before passing it as a dependency:
In the example above, if the `text` did not change, the `searchOptions` object also won't change. However, an even better fix is to move the `searchOptions` object declaration *inside* of the `useMemo` calculation function:
```js {2-7,11}
```js {3-4,6}
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]); // ✅ Memoization works
const visibleItems = useMemo(() => {
// ✅ This object is created inside useMemo
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Memoization works
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...
```
In this example, the `searchOptions` only gets recalculated when the `text` changes. Then, the `visibleItems` only gets recalculated if either `allItems` or `searchOptions` changes.
You can use a similar approach to prevent [`useEffect`](/api/react/useEffect) from firing again unnecessarily. However, there are usually better solutions than wrapping Effect dependencies in `useMemo`. Read about [removing Effect dependencies.](/learn/removing-effect-dependencies)
**Now your calculation depends on `text` directly (which is a string and can't "accidentally" be new like an object).**
You can use a similar approach to prevent [`useEffect`](/api/react/useEffect) from firing again unnecessarily. Before you try to optimize dependencies with `useMemo`, see if you can make them unnecessary. [Read about removing Effect dependencies.](/learn/removing-effect-dependencies)
---
@ -1037,10 +1015,13 @@ You can use a similar approach to prevent [`useEffect`](/api/react/useEffect) fr
Suppose the `Form` component is wrapped in [`memo`.](/api/react/memo) You want to pass a function to it as a prop:
```js {2-4}
export default function Page({ productId }) {
function handleSubmit(data) {
sendData(productId, data);
```js {2-7}
export default function ProductPage({ product, referrerId }) {
function handleSubmit(orderDetails) {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}
return <Form onSubmit={handleSubmit} />;
@ -1051,25 +1032,31 @@ Similar to how `{}` always creates a different object, function declarations lik
To memoize a function with `useMemo`, your calculation function would have to return another function:
```js {2-3,5-6}
export default function Page({ productId }) {
```js {2-3,8-9}
export default function Page({ product, referrerId }) {
const handleSubmit = useMemo(() => {
return (data) => {
sendData(productId, data);
return (orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
};
}, [productId]);
}, [product, referrerId]);
return <Form onSubmit={handleSubmit} />;
}
```
This looks clunky! Memoizing functions is common enough that React has a built-in Hook specifically for that. Wrap your functions into [`useCallback`](/apis/react/useCallback) instead of `useMemo` to avoid having to write an extra nested function:
This looks clunky! **Memoizing functions is common enough that React has a built-in Hook specifically for that. Wrap your functions into [`useCallback`](/apis/react/useCallback) instead of `useMemo`** to avoid having to write an extra nested function:
```js {2,4}
export default function Page({ productId }) {
const handleSubmit = useCallback(data => {
sendData(productId, data);
}, [productId]);
```js {2,7}
export default function Page({ product, referrerId }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + product.id + '/buy', {
orderDetails,
referrerId
});
}, [product, referrerId]);
return <Form onSubmit={handleSubmit} />;
}
@ -1136,7 +1123,7 @@ During subsequent renders, it will either return an already stored value from th
* `useMemo` is a Hook, so you can only call it **at the top level of your component** or your own Hooks. You can't call it inside loops or conditions. If you need that, extract a new component and move the state into it.
* In Strict Mode, React will **call your calculation function twice** in order to [help you find accidental impurities.](#my-calculation-runs-twice-on-every-re-render) This is development-only behavior and does not affect production. If your calculation function is pure (as it should be), this should not affect the logic of your component. The result from one of the calls will be ignored.
* React **will not throw away the cached value unless there is a specific reason to do that.** For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache--for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should match your expectations if you rely on `useMemo` solely as a performance optimization. Otherwise, a [state variable](apis/react/useState#avoiding-recreating-the-initial-state) or a [ref](/apis/react/useRef#avoiding-recreating-the-ref-contents) may be more appropriate.
* React **will not throw away the cached value unless there is a specific reason to do that.** For example, in development, React throws away the cache when you edit the file of your component. Both in development and in production, React will throw away the cache if your component suspends during the initial mount. In the future, React may add more features that take advantage of throwing away the cache--for example, if React adds built-in support for virtualized lists in the future, it would make sense to throw away the cache for items that scroll out of the virtualized table viewport. This should match your expectations if you rely on `useMemo` solely as a performance optimization. Otherwise, a [state variable](/apis/react/useState#avoiding-recreating-the-initial-state) or a [ref](/apis/react/useRef#avoiding-recreating-the-ref-contents) may be more appropriate.
---
@ -1190,7 +1177,6 @@ Also, check out the guides on [updating objects](/learn/updating-objects-in-stat
---
### My `useMemo` call is supposed to return an object, but returns undefined {/*my-usememo-call-is-supposed-to-return-an-object-but-returns-undefined*/}
This code doesn't work:

3
beta/src/sidebarReference.json

@ -91,8 +91,7 @@
},
{
"title": "useCallback",
"path": "/apis/react/useCallback",
"wip": true
"path": "/apis/react/useCallback"
},
{
"title": "useContext",

Loading…
Cancel
Save