Как использовать хук useMemo и useCallback в React
По мере роста ваших навыков работы с React вы будете все больше и больше заботиться о том, насколько хорошо работает ваше приложение. Когда дело доходит до оптимизации приложений React, кэширование так же важно, как и любой другой инструмент или метод программирования. В React кэширование обычно называют мемоизацией, и оно используется для уменьшения количества раз, когда компонент рендерится из-за изменений состояния или реквизита. В этой статье объясняется, как использовать хуки useMemo и useCallback в React для кэширования.
Кэширование по умолчанию в React
По умолчанию React использует метод, называемый "поверхностным сравнением", чтобы решить, следует ли повторно отображать компонент. Библиотека сравнивает предыдущие реквизиты и состояние компонента с новыми, и если изменений нет, React предполагает, что компонент не изменился, и не будет повторно отображать.
Для сложных компонентов с расширенным управлением состояния работа механизма по умолчанию недостаточна. Необходимы дополнительные методы кэширования для оптимизации производительности. Крючки useMemo и useCallback помогут справиться с ситуацией.
Хук useMemo
Примечание: хук useMemo возвращает только значение.
Рендеринг компонента выполняется полным кодом внутри него, включая функции, размещенные внутри компонента. При выполнении функцией сложных вычислений, выполненять операции при каждом рендеринге окажется непродуктивно, особенно если аргументы функции не изменились.
React перерисовывает компонент, если свойства или состояние изменились.
Здесь в игру вступает хук useMemo. Обернув функцию в хук useMemo, React запомнит вычисленное значение функции между рендерингами. Функция будет вызываться только в том случае, если какая-либо из зависимостей useMemo, указанных в массиве зависимостей (второй аргумент useMemo), изменилась. Если зависимости остаются такими же, как и при предыдущем рендеринге, возвращается ранее вычисленное значение, и необходимость в перерасчете отпадает.
Пример без useMemo:
Здесь вы можете видеть, что {expensiveCalculation(count)}
будет повторно отображаться независимо от того, щелкнете ли вы incrementCount
или incrementCountRender
, потому что React повторно отображает компонент, если свойство или состояние изменились. Это приводит к тому, что все внутри JSX return()
будет отображаться снова.
import React, { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [countRender, setCountRender] = useState(1)
const incrementCount = () => {
console.log('Increment count')
setCount((c) => c + 1);
};
const incrementCountRender = () => {
console.log('Increment count render')
setCountRender((c) => c + 1)
}
return (
<div>
<div>
Count: {count}
<button onClick={incrementCount}>+</button>
<br />
Count render: {countRender}
<button onClick={incrementCountRender}>+</button>
<h2>Expensive Calculation</h2>
{expensiveCalculation(count)}
</div>
</div>
);
};
const expensiveCalculation = (num: number) => {
console.log("Expensive calculation");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
export default App
Пример с useMemo:
Я удалил {expensiveCalculation(count)}
из JSX return()
и добавил функцию calculateResult
, которая обернута с помощью useMemo и возвращает expensiveCalculation(count)
с массивом зависимостей, внутри которого есть count
. Эта функция useMemo будет работать только при начальном рендеринге и при изменении count
.
import React, { useMemo, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [countRender, setCountRender] = useState(1)
const incrementCount = () => {
console.log('Increment count')
setCount((c) => c + 1);
};
const incrementCountRender = () => {
console.log('Increment count render')
setCountRender((c) => c + 1)
}
const calculatedResult = useMemo(() => {
console.log('useMemo')
return expensiveCalculation(count)
}, [count])
return (
<div>
<div>
Count: {count}
<button onClick={incrementCount}>+</button>
<br />
Count render: {countRender}
<button onClick={incrementCountRender}>+</button>
<h2>Expensive Calculation</h2>
{calculatedResult}
</div>
</div>
);
};
const expensiveCalculation = (num: number) => {
console.log("Expensive calculation");
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};
export default App
Хук useCallback
Примечание: хук useCallback возвращает функцию.
Функция useCallback в React позволяет запоминать функцию, гарантируя, что ссылка на функцию остается неизменной между рендерами, если только не изменились какие-либо зависимости, указанные в массиве зависимостей (второй аргумент useCallback).
При рендеринге компонента выполняется весь код внутри него, включая создание любых функций внутри компонента. Если функция передается в качестве реквизита дочерним компонентам, это может привести к ненужному повторному отображению этих дочерних компонентов, если ссылка на функцию изменится, даже если логика внутри функции не изменилась.
Пример без useCallback
Функция getTodos извлекает одну задачу из массива задач на основе состояния счетчика. Затем эта функция передается в качестве реквизита компоненту TodosList, где она используется в хуке useEffect для обновления состояния путем распространения полученной задачи на существующие задачи.
Эта функциональность работает правильно, когда нет состояния countTwo или кнопки setCountTwo, так как они запускают повторный рендеринг компонента. Если кнопка нажата, это вызовет повторную визуализацию компонента TodosList, что приведет к добавлению старой задачи в состояние задач, что может привести к ошибке.
import React, { useEffect, useState } from "react";
const todos = [
{id: 1, title: 'Todo 1'},
{id: 2, title: 'Todo 2'},
{id: 3, title: 'Todo 3'},
{id: 4, title: 'Todo 4'},
{id: 5, title: 'Todo 5'},
]
const App = () => {
const [count, setCount] = useState(0);
const [countTwo, setCountTwo] = useState(100)
const getTodos = () => {
return todos[count]
}
return (
<div>
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>+</button>
<br />
Count Two: {countTwo}
<button onClick={() => setCountTwo(countTwo - 1)}>+</button>
<br />
<TodosList getTodos={getTodos}/>
</div>
</div>
);
};
const TodosList = ({getTodos}) => {
const [todos, setTodos] = useState([])
useEffect(() => {
console.log('getTodos function is called.')
setTodos([...todos, getTodos()])
}, [getTodos])
return (
<>
{todos.map(todo => <span key={todo.id}>{todo.title} </span>)}
</>
)
}
export default App
Пример с useCallback
Здесь мы обернули функцию getTodos в хук useCallback. Это гарантирует, что функция будет выполняться только во время первоначального рендеринга компонента или при изменении состояния счетчика. В противном случае он не будет вызван.
import React, { useCallback, useEffect, useState } from "react";
const todos = [
{id: 1, title: 'Todo 1'},
{id: 2, title: 'Todo 2'},
{id: 3, title: 'Todo 3'},
{id: 4, title: 'Todo 4'},
{id: 5, title: 'Todo 5'},
]
const App = () => {
const [count, setCount] = useState(0);
const [countTwo, setCountTwo] = useState(100)
const getTodos = useCallback(() => {
return todos[count]
}, [count])
return (
<div>
<div>
Count: {count}
<button onClick={() => setCount(count + 1)}>+</button>
<br />
Count Two: {countTwo}
<button onClick={() => setCountTwo(countTwo - 1)}>+</button>
<br />
<TodosList getTodos={getTodos}/>
</div>
</div>
);
};
const TodosList = ({getTodos}) => {
const [todos, setTodos] = useState([])
useEffect(() => {
console.log('getTodos function is called.')
setTodos([...todos, getTodos()])
}, [getTodos])
return (
<>
{todos.map(todo => <span key={todo.id}>{todo.title} </span>)}
</>
)
}
export default App
Вывод
useMemo: позволяет вычислять и сохранять значение только тогда, когда изменились его зависимости. Запоминая значение, React может избежать ненужных пересчетов и повторного отображения компонента, повышая общую производительность.
useCallback: используется для запоминания функции. Он возвращает запоминаемую версию функции, которая будет вызвана только в том случае, если ее зависимости были изменены. Это особенно полезно при передаче обратных вызовов в качестве реквизита дочерним компонентам.
Разумно используя useMemo и useCallback, вы можете оптимизировать свое приложение React за счет сокращения ненужных вычислений и рендеринга, что приведет к повышению производительности и более плавному взаимодействию с пользователем.