Архитектура компонентного дерева без каскадных ререндеров
Как избежать каскадных ререндеров в React: практические приемы мемоизации, локального состояния и селекторов контекста.
Если ваш список дергается, а панель фильтров перерисовывается при изменении любого поля, скорее всего, вы столкнулись с каскадными ререндеры. Профилирование покажет, где происходит лишняя работа, а правильная архитектура как ее избавиться.
Что вызывают каскадные ререндеры?
Каскадные ререндеры это ситуация, когда изменение состояния в одном узле заставляет React пере-рендерить всю ветку, иногда и все приложение. Чаще всего виноваты “широкие” пропсы, неэффективные контексты и отсутствие мемоизации.
Признак | Что происходит | Как исправить |
---|---|---|
Пропсы передаются “сквозными” уровнями | Каждый уровень получает новый объект и перерисовывается | Выделить состояние в более низкие компоненты или использовать React.memo |
Контекст меняется часто | Все подписчики получают новые значения | Делить контекст на части, использовать селекторы (см. ниже) |
Функции создаются в рендере | При каждом рендере новые ссылки → дочерние memo -компоненты считают пропсы измененными | Обернуть функцию в useCallback |
Разделяем состояние: локальное vs глобальное
Самый простой способ сократить “тучу” перерисовок хранить состояние как можно ближе к тем компонентам, которые его используют. Если глобальный store (Redux, Zustand) содержит большой объект, каждый раз при обновлении части этого объекта все подписчики получают новые пропсы.
// Плохой вариант: один большой store
import create from "zustand";
const useStore = create((set) => ({
filter: "",
items: [],
sort: "asc",
// …
}));
function App() {
const { filter, items, sort } = useStore(); // каждый ререндер новые ссылки
return <>...</>;
}
Практика
- Локальное состояние храните
filter
в компоненте фильтра, аitems
в списке. - Селекторы в Zustand:
useStore(state => state.filter)
; в Redux:useSelector(state => state.filter)
. - Контекст только для “темных” данных например, тема оформления, а не список продуктов.
Подробнее о мемоизации компонентов можно почитать в мемоизация компонентов.
Мемоизация компонентов и функций
React.memo
ваш первый щит против каскадных ререндеров. Он сравнивает текущие и предыдущие пропсы “поверхностно”. Если пропсы объекты, стоит добавить useMemo
/useCallback
.
import React, { useMemo } from "react";
const Item = React.memo(({ data }: { data: { id: string; title: string } }) => {
console.log("render Item", data.id);
return <li>{data.title}</li>;
});
function List({ items }: { items: Array<{ id: string; title: string }> }) {
// memoize массив → ссылка меняется только при реальных изменениях
const memoizedItems = useMemo(() => items, [items]);
return (
<ul>
{memoizedItems.map((item) => (
<Item key={item.id} data={item} />
))}
</ul>
);
}
Обратите внимание, что useMemo
здесь не “кеширует” массив, а гарантирует стабильную ссылку, что спасает от лишних рендеров Item
.
Если список большой, рассмотрите виртуализацию списка она избавит от рендера элементов, не попавших в viewport.
Контекст и селекторы: минимизируем подпитку
Контекст удобен, но частое изменение заставляет всех подписчиков перерисовываться. Разделите контекст и подпишитесь только на нужные свойства через селектор.
import React, { useState, useMemo } from "react";
import { useContextSelector } from "use-context-selector";
// ThemeContext хранит только тему
const ThemeContext = React.createContext<{
mode: string;
setMode: React.Dispatch<React.SetStateAction<string>>;
}>({
mode: "light",
setMode: () => {},
});
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = useState("light");
const value = useMemo(() => ({ mode, setMode }), [mode]);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
function ThemedButton() {
// селектор "mode" обновляемся только при смене темы
const mode = useContextSelector(ThemeContext, (ctx) => ctx.mode);
return <button className={mode}>Click</button>;
}
Библиотека use-context-selector
(или аналог в React 18) позволяет подписываться только на нужные свойства, тем самым устраняя каскадные ререндеры от глобального контекста.
Чеклист: чистое дерево компонентов
- Компоненты-мемы оберните каждый “тяжелый” компонент в
React.memo
. - Функции в пропсах оборачивайте в
useCallback
. - Объекты/массивы в пропсах кешируйте через
useMemo
. - Контекст держите в нем только статичные данные или используйте селекторы.
- Профайлинг откройте React DevTools → Profiler, найдите горячие зоны и примените приемы выше.
Каскадные ререндеры почти всегда следствие избыточного положения состояния и отсутствия мемоизации.
Правильная архитектура компонентного дерева делает UI плавным, а каждое обновление происходит только тогда, когда действительно нужно. Это особенно ценно в больших приложениях с интерактивными списками и сложными формами.
Для более глубокого понимания процессов рендеринга изучите рендеринг и реконсиляцию в React.