Все мы знаем, что с появлением React 16.8, в нашу жизнь пришли хуки. Действительно, они упрощают процесс написания кода, используя функциональные компоненты в сравнении с классами, но некоторые хуки вызывают много вопросов у разработчиком. В данной статье мы рассмотрим на примерах, когда же нужно использовать useCallback, а когда можно обойтись без него.
Мы знаем из документации, что useCallback возвращает мемоизированную версию коллбэка. Простыми словами возвращает одну и туже ссылку на функцию, до тех пор, пока не изменится одна из зависимостей.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Давайте рассмотрим пример:
У нас имеется страница с инпутом, списком отображения элементов и кнопкой, которая добавляет в список введенный элемент. При клике на сам элемент списка, он будет удаляться. Дефолтный список возьмём из 5 элементов, который мы будем редактировать.
const listOfCities = ['Beijing','Tokyo','Kinshasa','Moscow','Jakarta'];
const Page = () => {
const [name, setName] = useState("");
const [list, setList] = useState(listOfCities);
const handleClick = () => {
setList([...list, name]);
setName("");
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const handleRemoveClick = (item: string) => {
const filteredList = list.filter((listItem) => listItem !== item);
setList(filteredList);
};
console.log("Page render");
return (
<div>
<input type="text" value={name} onChange={handleChange} />
<button onClick={handleClick}>Add</button>
<CitiesList list={list} onRemoveClick={handleRemoveClick} />
</div>
);
};
И компоненты отображения списка
export const CitiesList = ({list, onRemoveClick}) => {
console.log("List render");
return list.map((item) => {
return <City key={item} city={item} onRemoveClick={onRemoveClick} />;
});
};
export const City = ({city, onRemoveClick}) => {
const handleCityClick = () => onRemoveClick(city);
console.log("Element render");
return <div onClick={handleCityClick}>{city}</div>;
};
Итак, какие же есть сейчас проблемы с перформансом. Давайте запустим и посмотрим что у нас получилось, и сразу откроем консоль разработчика.
Кажется все логично, произошел рендер страницы, компонента списка и его 5 элементов.
Начинаем вводить в инпут символы и видим, что на каждый введенный символ, происходит аналогичный рендер во всех компонентах. Изменения значения инпут (state name), вызывает рендер во всех дочерних компонентах.
У нас есть несколько callback функции, которые теоретически можно обернуть в useCallback. Разберем их по отдельности.
Обернув handleClick, handleChange или handleCityClick в useCallback, увы это никак не улучшит перфоманс, ведь useCallback это тоже функция, которая при каждом рендер, будет заново сравнивать зависимости и возвращать новую или старую ссылку на функцию.
Обернув handleRemoveClick в useCallback, это уменьшит количество ререндеров при условии что мы обернем СitiesList в React.memo, подробнее о нем можно посмотреть в документации.
const handleRemoveClick = useCallback(
(item: string) => {
const filteredList = list.filter((listItem) => listItem !== item);
setList(filteredList);
},
[list]
);
const CitiesList = React.memo(({ list, onRemoveClick }) => {
console.log("List render");
return list.map((item) => {
return <City key={item} item={item} onRemoveClick={onRemoveClick} />;
});
});
Теперь при вводе символов в инпуте, компонент СitiesList не перерендеривается. На список из 5 элементов улучшение перформанса будет не столь большим, но наглядно показывает как его можно будет улучшить в случае необходимости.
В данном примере можно было вынести инпут с кнопкой и его state в отдельный компонент, тогда необходимость использования useCallback уже была бы не актуальной, поэтому не делайте оптимизаций производительности до тех пор, пока это действительно не потребуется.
Какие из этого можно сделать вывод
Встроенный хук useCallback нужная и полезная вещь, которая помогает улучшить перформанс. Но её применение не всегда актуально. Используйте его для функций, которые передаются компонентам с большими затратами памяти для отображения. Делайте оптимизацию производительности после написания и рефакторинга кода.
Надеюсь, эта статья была вам полезной. Всем пока :)
Комментарии (11)
Pavel1114
22.11.2021 08:00+1Полезное видео по теме от АйТи-Синяка www.youtube.com/watch?v=2Wp7QPTkpms
habrjeka
22.11.2021 10:23+2Проблема: производительность - рендер Page и всех его дочерних компонентов каждый раз при вводе в input.
Причина: вызывается setState в handleChange при onChange input элемента
Решение: убираем состояние useState() для name;
без всяких useCallback
import React, { useState } from "react"; function Page() { const listOfCities = ["Beijing", "Tokyo", "Kinshasa", "Moscow", "Jakarta"]; const [list, setList] = useState(listOfCities); let newItem = ""; const handleClick = () => { setList([...list, newItem]); newItem = ""; }; const handleChange = (event) => { newItem = event.target.value; }; const handleRemoveClick = (item) => { const filteredList = list.filter((listItem) => listItem !== item); setList(filteredList); }; console.log("Page render"); return ( <div> <input type="text" onChange={handleChange} /> <button onClick={handleClick}>Add</button> <CitiesList list={list} onRemoveClick={handleRemoveClick} /> </div> ); } export const CitiesList = ({ list, onRemoveClick }) => { console.log("List render"); return list.map((item) => { return <City key={item} city={item} onRemoveClick={onRemoveClick} />; }); }; export const City = ({ city, onRemoveClick }) => { const handleCityClick = () => onRemoveClick(city); console.log("City render"); return <div onClick={handleCityClick}>{city}</div>; }; export default Page;
Пример в статье подобран неудачно ...
kahi4
22.11.2021 11:38+2А теперь вбейте инпут, удалите город из списка, а потом нажмите add.
Но согласен, что пример не самый удачный
habrjeka
22.11.2021 11:42Шах и мат :)
Roman9131 Автор
22.11.2021 23:10+1@habrjeka Спасибо за комментарии, как написано в конце статьи, действительно, данную проблему производительности можно решить путем вынесением поля ввода вместе с его state в отдельный компонент или тем методом который был предложен выше в комментарии. Моя идея была все таки не предложить идеальное решение этой задачи, а показать как можно было бы ее решить при помощи useCallback
mobilz
23.11.2021 03:26о господи, никогда так не делайте. при любом чихе вы будете обнулять свою переменную и контролировать это никак не получится. т.е. обновился родительский компонент -- обнулили переменную, даже если вы набирали текст в моменте.
ну и не используйте let, забудьте на годик про let и var, используйте const
strannik_k
23.11.2021 18:30Еще можно в некоторых случаях выносить функции вне компонента, а не помещать их в useCallback. Тогда будет не нужно беспокоиться о лишних перерисовках. К тому же, за счет вынесения кода, большие компоненты станут гораздо читабельнее.
skeevy
01.12.2021 18:38const [list, setList] = useState(listOfCities); const handleClick = () => { setList([...list, newItem]); };
я надеюсь, в проде вы так не делаете, как написали на примере. Разворачивать массив еще раз, да еще и по результирующей переменной вместо использования предыдущего состояния может вам очень больших проблем подкинуть
romanere
Спасибо за демонстрацию примера, но переменные в коде написаны на русских и английских символах. Если кто-то захочет скопировать и протестировать, у него ничего не выйдет.
Рабочий код без русских символов
Roman9131 Автор
@romanere Спасибо за внимательность, поправил переменные