Пошук уроків, статей та іншого контенту
Навчитеся стабілізувати посилання на функції через useCallback і правильно поєднувати його з мемоізованими компонентами.
У JavaScript функції є об’єктами. Кожне створення функції створює нове посилання в пам’яті:
const first = () => {};
const second = () => {};
console.log(first === second); // falseУ React функція компонента виконується знову під час кожного оновлення стану або пропсів. Тому функції, оголошені всередині компонента, також створюються заново:
function Counter() {
const handleClick = () => {
console.log("Натискання");
};
// handleClick має нове посилання після кожного рендеру
return <button onClick={handleClick}>Натиснути</button>;
}Для самого DOM це зазвичай не є проблемою. Проте нове посилання на функцію важливе, якщо:
функція передається дочірньому компоненту;
дочірній компонент обгорнуто в React.memo;
функція використовується як залежність у useEffect, useMemo або іншому хуку.
useCallback дозволяє зберігати те саме посилання на функцію між рендерами, доки не змінюються її залежності.
useCallbackconst memoizedFunction = useCallback(function, dependencies);Наприклад:
const handleClick = useCallback(() => {
console.log("Натискання");
}, []);React повертає те саме посилання на функцію під час наступних рендерів, якщо масив залежностей не змінився.
Другий аргумент працює так само, як масив залежностей у useEffect:
[] — функція не змінюється між рендерами;
[value] — функція створюється заново, коли змінюється value;
[value, anotherValue] — функція залежить від обох значень.
useCallbackне запобігає виконанню функції. Він стабілізує лише посилання на неї.
React.memo і стабільні пропсиReact.memo пропускає повторний рендер дочірнього компонента, якщо його пропси не змінилися за поверхневим порівнянням.
Розглянемо приклад:
import { useState } from "react";
function Button({ onClick }) {
console.log("Button render");
return <button onClick={onClick}>Збільшити</button>;
}
const MemoizedButton = React.memo(Button);Якщо передати функцію без useCallback, після кожного рендеру батьківського компонента з’являтиметься нове посилання:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((currentCount) => currentCount + 1);
};
return (
<>
<p>{count}</p>
<MemoizedButton onClick={increment} />
</>
);
}Навіть якщо MemoizedButton обгорнуто в React.memo, він рендеритиметься знову. Причина — increment щоразу є новою функцією.
Поєднаймо React.memo з useCallback:
import { memo, useCallback, useState } from "react";
const MemoizedButton = memo(function Button({ onClick }) {
console.log("Button render");
return <button onClick={onClick}>Збільшити</button>;
});
export default function Counter() {
const [count, setCount] = useState(0);
const [theme, setTheme] = useState("light");
const increment = useCallback(() => {
setCount((currentCount) => currentCount + 1);
}, []);
return (
<div className={theme}>
<p>Лічильник: {count}</p>
<button
onClick={() => {
setTheme((currentTheme) =>
currentTheme === "light" ? "dark" : "light"
);
}}
>
Змінити тему
</button>
<MemoizedButton onClick={increment} />
</div>
);
}Тепер під час зміни theme:
Counter рендериться знову;
посилання на increment залишається тим самим;
MemoizedButton може пропустити повторний рендер.
Коли змінюється count, Counter також рендериться знову, але в цьому випадку кнопка не отримує нове посилання на onClick.
useCallbackФункція повинна мати в залежностях усі зовнішні значення, які вона читає.
function UserProfile({ userId }) {
const loadUser = useCallback(() => {
console.log(`Завантаження користувача ${userId}`);
}, [userId]);
return <button onClick={loadUser}>Завантажити</button>;
}Тут loadUser використовує userId, тому userId має бути в масиві залежностей. Коли userId зміниться, React створить нову функцію з актуальним значенням.
Неправильний варіант:
function UserProfile({ userId }) {
const loadUser = useCallback(() => {
console.log(`Завантаження користувача ${userId}`);
}, []);
return <button onClick={loadUser}>Завантажити</button>;
}У цьому випадку функція може продовжити використовувати старе значення userId. Таку проблему називають замиканням на застарілому значенні, або stale closure.
Якщо callback оновлює стан на основі його попереднього значення, використовуйте функціональну форму setter-а:
const increment = useCallback(() => {
setCount((currentCount) => currentCount + 1);
}, []);Завдяки цьому callback не читає count безпосередньо, тому count не потрібно додавати до залежностей.
Порівняйте з іншим варіантом:
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);Цей код також правильний, але increment створюватиметься заново після кожної зміни count, оскільки count є його залежністю.
Функціональний setter часто допомагає зменшити кількість залежностей:
import { memo, useCallback, useState } from "react";
const AddTodoButton = memo(function AddTodoButton({ onAdd }) {
console.log("AddTodoButton render");
return (
<button onClick={() => onAdd("Нова задача")}>
Додати задачу
</button>
);
});
export default function TodoList() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState("all");
const addTodo = useCallback((title) => {
setTodos((currentTodos) => [
...currentTodos,
{
id: crypto.randomUUID(),
title,
},
]);
}, []);
const visibleTodos = todos.filter((todo) => {
if (filter === "all") {
return true;
}
return todo.title.toLowerCase().includes(filter.toLowerCase());
});
return (
<section>
<input
value={filter === "all" ? "" : filter}
placeholder="Фільтр"
onChange={(event) => setFilter(event.target.value || "all")}
/>
<AddTodoButton onAdd={addTodo} />
<ul>
{visibleTodos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</section>
);
}addTodo стабільний, тому зміна filter не змушує мемоізовану кнопку рендеритися через зміну її пропса onAdd.
Callback може залежати не лише від стану, а й від пропсів:
import { memo, useCallback } from "react";
const SaveButton = memo(function SaveButton({ onSave }) {
return <button onClick={onSave}>Зберегти</button>;
});
function Editor({ documentId }) {
const saveDocument = useCallback(() => {
console.log(`Збереження документа ${documentId}`);
}, [documentId]);
return <SaveButton onSave={saveDocument} />;
}Якщо documentId не змінюється, saveDocument має стабільне посилання. Якщо змінюється — callback повинен змінитися, щоб працювати з новим документом.
useCallback не робитьuseCallback:
не кешує результат виконання функції;
не робить функцію швидшою;
не зупиняє рендер батьківського компонента;
не зупиняє рендер дочірнього компонента, якщо інші його пропси змінилися;
не потрібен для кожної функції в компоненті.
Для кешування результату обчислення використовують useMemo, а useCallback застосовують саме для стабілізації посилання на функцію.
useCallbackuseCallback доречний, коли:
Callback передається компоненту, обгорнутому в React.memo.
Callback використовується як залежність іншого хука.
Компонент часто рендериться, а переданий callback спричиняє зайві рендери дочірнього компонента.
Стабільне посилання на функцію потрібне для конкретної оптимізації.
Приклад із React.memo має сенс, якщо дочірній компонент справді може пропускати рендер:
const MemoizedItem = memo(function Item({ item, onSelect }) {
return (
<button onClick={() => onSelect(item.id)}>
{item.title}
</button>
);
});У батьківському компоненті:
const handleSelect = useCallback((id) => {
console.log("Вибрано:", id);
}, []);Утім, якщо дочірній компонент все одно повинен рендеритися через зміну інших пропсів, стабілізація одного callback не дасть помітного ефекту.
useCallback не потрібенНе варто механічно обгортати всі функції в useCallback:
function Greeting({ name }) {
const getMessage = useCallback(() => {
return `Привіт, ${name}!`;
}, [name]);
return <p>{getMessage()}</p>;
}У цьому прикладі функція використовується лише всередині того самого компонента. Її стабільне посилання не приносить користі, тому простіше написати:
function Greeting({ name }) {
const getMessage = () => `Привіт, ${name}`;
return <p>{getMessage()}</p>;
}useCallback також має власні витрати: React повинен зберігати callback і перевіряти його залежності. Тому його слід використовувати для обґрунтованої оптимізації, а не як обов’язковий стиль написання коду.
const handleSearch = useCallback(() => {
search(query);
}, []);Якщо query змінюється, callback може використовувати неактуальне значення.
Правильно:
const handleSearch = useCallback(() => {
search(query);
}, [query]);React.memoReact.memo порівнює всі пропси, а не лише callback:
<MemoizedButton
onClick={stableHandler}
style={{ color: "blue" }}
/>Об’єкт style створюється заново під час кожного рендеру. Навіть стабільний stableHandler не допоможе, якщо інший пропс змінив посилання.
<MemoizedButton onClick={() => handleClick(id)} />Ця функція створюється заново під час кожного рендеру. Якщо важливо зберегти стабільне посилання, callback можна підготувати заздалегідь, передавши дочірньому компоненту дані й стабільну функцію:
const handleClick = useCallback((id) => {
console.log(id);
}, []);
<MemoizedButton itemId={id} onClick={handleClick} />;Дочірній компонент:
function MemoizedButton({ itemId, onClick }) {
return <button onClick={() => onClick(itemId)}>Вибрати</button>;
}Порожній масив означає: «ця функція не залежить від значень, які можуть змінюватися». Якщо це не так, виникає ризик застарілих даних.
Перед використанням [] перевірте, які змінні callback читає із зовнішньої області видимості.
useCallback без вимірюванняЯкщо оптимізація потрібна через повільний рендер, спочатку визначте, який компонент рендериться зайво. useCallback має сенс, коли стабільність функції дозволяє React.memo або іншому механізму пропустити непотрібну роботу.
Визначте, чи передається функція дочірньому компоненту.
Перевірте, чи дочірній компонент обгорнуто в React.memo.
Переконайтеся, що інші його пропси також не змінюються без потреби.
Випишіть усі зовнішні значення, які читає callback.
Додайте їх до масиву залежностей.
Якщо оновлюєте стан на основі попереднього стану, використайте функціональний setter.
Переконайтеся, що оптимізація справді зменшує кількість зайвих рендерів.
Функції, оголошені в компоненті, отримують нове посилання після кожного рендеру.
useCallback зберігає посилання на функцію, доки не зміняться її залежності.
Найчастіше useCallback використовують разом із React.memo.
Усі значення, які callback читає з зовнішньої області, потрібно враховувати в залежностях.
Функціональний setter дозволяє оновлювати стан без прямої залежності від поточного значення.
useCallback не прискорює функцію і не потрібен для кожного обробника.
Стабільний callback корисний лише тоді, коли він допомагає пропустити справді зайвий рендер або підтримує коректні залежності інших хуків.