Пошук уроків, статей та іншого контенту
Навчимося застосовувати memo для пропуску непотрібних ререндерів компонентів і розуміти його обмеження.
memomemo — це оптимізація для функціональних компонентів React. Вона дозволяє пропустити повторний рендеринг компонента, якщо його пропси не змінилися.
const OptimizedComponent = memo(Component);Без memo дочірній компонент зазвичай повторно рендериться, коли повторно рендериться його батьківський компонент. Це відбувається навіть тоді, коли дочірні пропси залишилися такими самими.
memo порівнює нові пропси з попередніми. Якщо React вважає їх однаковими, тіло компонента не виконується повторно.
memo— це оптимізація продуктивності, а не спосіб змінити логіку компонента.
memoimport { useState } from "react";
function UserCard({ user }) {
console.log("UserCard: рендер");
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
</article>
);
}
export default function App() {
const [count, setCount] = useState(0);
const user = {
name: "Олена",
email: "olena@example.com",
};
console.log("App: рендер");
return (
<main>
<button onClick={() => setCount((value) => value + 1)}>
Лічильник: {count}
</button>
<UserCard user={user} />
</main>
);
}Після кожного натискання кнопки змінюється стан App. Через це App рендериться повторно, а разом із ним рендериться UserCard.
У цьому прикладі дані користувача візуально не змінюються, але UserCard усе одно виконує свій код повторно.
memoКомпонент можна передати до memo:
import { memo, useState } from "react";
const UserCard = memo(function UserCard({ user }) {
console.log("UserCard: рендер");
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
</article>
);
});
export default function App() {
const [count, setCount] = useState(0);
const user = {
name: "Олена",
email: "olena@example.com",
};
console.log("App: рендер");
return (
<main>
<button onClick={() => setCount((value) => value + 1)}>
Лічильник: {count}
</button>
<UserCard user={user} />
</main>
);
}На перший погляд здається, що тепер UserCard не буде рендеритися повторно. Але це не так.
На кожному рендері App створюється новий об'єкт:
const user = {
name: "Олена",
email: "olena@example.com",
};У JavaScript два об'єкти з однаковим вмістом — це різні значення:
const firstUser = { name: "Олена" };
const secondUser = { name: "Олена" };
console.log(firstUser === secondUser); // falseТому memo бачить зміну пропса user.
За замовчуванням memo порівнює кожен пропс окремо приблизно так:
Object.is(previousProp, nextProp);Для примітивних значень це зазвичай відповідає очікуванням:
"admin" === "admin"; // true
10 === 10; // true
true === true; // trueДля об'єктів, масивів і функцій порівнюється посилання:
{} === {}; // false
[] === []; // false
(() => {}) === (() => {}); // falseТому такі пропси можуть спричинити ререндеринг мемоізованого компонента:
<UserCard user={{ name: "Олена" }} />
<UserList items={["React", "JavaScript"]} />
<Button onClick={() => console.log("Натиснуто")} />На кожному рендері батьківського компонента створюються нові об'єкти, масиви або функції.
Для об'єктів і масивів можна використати useMemo, а для функцій — useCallback.
import { memo, useCallback, useMemo, useState } from "react";
const UserCard = memo(function UserCard({ user, onSelect }) {
console.log("UserCard: рендер");
return (
<article>
<h2>{user.name}</h2>
<p>{user.email}</p>
<button onClick={() => onSelect(user.id)}>
Вибрати користувача
</button>
</article>
);
});
export default function App() {
const [count, setCount] = useState(0);
const [selectedUserId, setSelectedUserId] = useState(null);
const user = useMemo(
() => ({
id: 1,
name: "Олена",
email: "olena@example.com",
}),
[]
);
const handleSelect = useCallback((userId) => {
setSelectedUserId(userId);
}, []);
console.log("App: рендер");
return (
<main>
<button onClick={() => setCount((value) => value + 1)}>
Лічильник: {count}
</button>
<p>
Обраний користувач:{" "}
{selectedUserId === null ? "немає" : selectedUserId}
</p>
<UserCard user={user} onSelect={handleSelect} />
</main>
);
}Тепер під час зміни count:
App рендериться повторно;
user отримує те саме посилання завдяки useMemo;
handleSelect отримує те саме посилання завдяки useCallback;
UserCard може пропустити ререндеринг завдяки memo.
Однак useMemo і useCallback не потрібно додавати до кожного значення автоматично. Вони теж мають власну вартість і потрібні передусім тоді, коли стабільне посилання важливе для оптимізації або іншої логіки.
memo і примітивні пропсиЯкщо компонент отримує примітивні пропси, мемоізація зазвичай простіша:
import { memo, useState } from "react";
const ProductPrice = memo(function ProductPrice({ price, currency }) {
console.log("ProductPrice: рендер");
return (
<p>
Ціна: {price} {currency}
</p>
);
});
export default function App() {
const [count, setCount] = useState(0);
return (
<main>
<button onClick={() => setCount((value) => value + 1)}>
Кліків: {count}
</button>
<ProductPrice price={999} currency="грн" />
</main>
);
}Під час зміни count пропси price і currency залишаються такими самими, тому ProductPrice може не рендеритися повторно.
Другим аргументом memo можна передати функцію порівняння:
const MemoizedComponent = memo(Component, arePropsEqual);Функція має повернути:
true, якщо новий і попередній набір пропсів можна вважати однаковим;
false, якщо компонент потрібно рендерити повторно.
Приклад:
import { memo } from "react";
const UserName = memo(
function UserName({ user }) {
console.log("UserName: рендер");
return <h2>{user.name}</h2>;
},
(previousProps, nextProps) => {
return previousProps.user.name === nextProps.user.name;
}
);У цьому випадку компонент не буде рендеритися повторно, якщо ім'я користувача не змінилося, навіть коли об'єкт user отримав нове посилання.
Власне порівняння варто використовувати обережно. Воно саме може бути складним або дорогим. Якщо порівняння великого масиву займає більше часу, ніж звичайний рендеринг компонента, така оптимізація не допоможе.
memomemo не забороняє ререндеринг через власний стан компонента:
import { memo, useState } from "react";
const Counter = memo(function Counter() {
const [count, setCount] = useState(0);
console.log("Counter: рендер");
return (
<button onClick={() => setCount((value) => value + 1)}>
Лічильник: {count}
</button>
);
});Коли count змінюється, Counter має рендеритися повторно. memo стосується пропсів, які компонент отримує від батьківського компонента.
Якщо мемоізований компонент використовує значення з Context, зміна цього Context може спричинити його ререндеринг. memo не блокує оновлення даних, від яких компонент безпосередньо залежить.
Цей компонент все одно може рендеритися повторно:
<MemoizedButton onClick={() => saveItem(item)} />Функція створюється заново під час кожного рендера батьківського компонента. Якщо це справді важливо для продуктивності, функцію можна стабілізувати за допомогою useCallback.
Аналогічна проблема виникає тут:
<MemoizedList items={[1, 2, 3]} />Навіть якщо елементи масиву не змінилися, сам масив є новим об'єктом.
childrenJSX, переданий через children, теж є значенням, яке може отримати нове посилання:
<MemoizedPanel>
<p>Вміст панелі</p>
</MemoizedPanel>Якщо батьківський компонент рендериться повторно, елемент <p> може бути створений заново. Через це memo не завжди пропустить ререндеринг MemoizedPanel.
Небезпечно ігнорувати пропси у функції порівняння без повного розуміння наслідків:
const Button = memo(
function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>;
},
(previousProps, nextProps) => {
return previousProps.label === nextProps.label;
}
);Тут функція порівняння ігнорує onClick. Якщо onClick змінився, компонент може зберегти стару функцію та виконувати застарілу логіку.
У функції порівняння потрібно враховувати всі пропси, які впливають на результат або поведінку компонента.
memomemo може бути корисним, коли:
компонент часто отримує повторний рендеринг від батьківського компонента;
його рендеринг є достатньо дорогим;
пропси компонента часто залишаються незмінними;
компонент відображається багато разів у великому списку;
стабільність посилань на об'єкти, масиви та функції вже забезпечена.
Не варто мемоізувати всі компоненти без аналізу. Для маленького компонента з простим JSX перевірка пропсів може не дати помітного виграшу.
Спочатку потрібно переконатися, що ререндеринг справді створює проблему, а потім застосовувати memo до відповідного компонента.
memo порівнює вміст об'єктівmemo не виконує глибоке порівняння об'єктів. Для об'єктів порівнюється посилання.
{ id: 1 } === { id: 1 }; // falsememo без стабільних пропсівЯкщо щоразу створювати нові об'єкти, масиви та функції, memo не зможе ефективно пропускати ререндери.
memo не є обов'язковою частиною кожного компонента. Надмірна мемоізація ускладнює код і не обов'язково покращує продуктивність.
Не можна змінювати об'єкти або масиви, які передані через пропси:
function UserCard({ user }) {
user.name = "Нове ім'я"; // помилка підходу
return <h2>{user.name}</h2>;
}React очікує, що пропси поводяться як незмінні значення. Зміни потрібно створювати через нові значення стану або нові об'єкти.
У режимі розробки React може виконувати додаткові рендери для виявлення проблем у коді, зокрема під час використання Strict Mode. Тому повідомлення в console.log не завжди точно показують поведінку production-збірки.
memo може пропустити ререндеринг функціонального компонента, якщо його пропси не змінилися.
За замовчуванням пропси порівнюються поверхнево через Object.is.
Примітиви порівнюються за значенням, а об'єкти, масиви та функції — за посиланням.
Для стабільності об'єктів і масивів можна використовувати useMemo, а для функцій — useCallback.
memo не блокує ререндеринг через власний стан або використані контексти.
Власну функцію порівняння потрібно писати обережно й враховувати всі важливі пропси.
memo — це оптимізація, яку варто застосовувати для реальної проблеми продуктивності, а не механічно для всіх компонентів.