Как проектировать глобальное хранилище, чтобы избежать проблем с избыточным ререндером?
Проблема избыточных ререндеров при использовании глобального хранилища в React-приложениях возникает тогда, когда любые изменения в состоянии вызывают обновление всех компонентов, которые его потребляют — даже если обновлённые данные им не нужны. Это критично для производительности, особенно в больших приложениях. Чтобы избежать этой проблемы, глобальное хранилище нужно проектировать так, чтобы изменения в состоянии были изолированными, селективными и контролируемыми.
Принципы проектирования глобального хранилища с фокусом на минимизацию ререндеров
1. Нормализация состояния
Состояние приложения должно быть нормализовано — как в базе данных. Это значит, что все сущности хранятся по id, и компонентам передаётся только нужная часть данных.
{
users: {
byId: {
1: { id: 1, name: 'Alice' },
2: { id: 2, name: 'Bob' }
},
allIds: \[1, 2\]
}
}
Преимущества:
-
Легко обновлять данные выборочно.
-
Компоненты подписываются только на нужные id.
2. Разделение стора на модули / слайсы
Каждая часть состояния должна иметь свой собственный "домен", особенно в Redux или Zustand. Не нужно хранить всё в одном большом объекте, если этого можно избежать.
// вместо:
store = {
auth: {...},
todos: {...},
ui: {...}
}
Почему это важно: при обновлении auth не должно происходить обновления todos, если компонент использует только их.
3. Избирательное чтение данных
Компоненты должны получать только ту часть глобального состояния, которая им действительно нужна.
В Redux:
Использовать reselect:
const selectVisibleTodos = createSelector(
\[state => state.todos, state => state.filter\],
(todos, filter) => todos.filter(todo => todo.status === filter)
);
В Zustand:
Можно использовать shallow или селекторы:
const taskCount = useStore(state => state.tasks.length);
В Jotai:
Можно создавать отдельные атомы для каждого свойства:
const userAtom = atom({ id: 1, name: 'Alice' });
const userNameAtom = atom(get => get(userAtom).name);
Преимущество: изменение name не вызовет обновление компонентов, использующих другие поля.
4. Изоляция логики через контексты
Не помещать всё глобальное состояние в один контекст. Разбивать по областям ответственности: AuthContext, ThemeContext, NotificationContext и т. д.
Плюсы:
-
Контекст обновляется только при изменении своих данных.
-
Легче тестировать и масштабировать.
5. Мемоизация селекторов и компонентов
Компоненты, подписанные на глобальный стейт, должны быть мемоизированы через React.memo, useMemo, useCallback — особенно при использовании Context.
const UserProfile = React.memo(({ user }) => {
return <div>{user.name}</div>;
});
const value = useMemo(() => ({ theme, setTheme }), \[theme\]);
<ThemeContext.Provider value={value}>...</ThemeContext.Provider>
Важно: при использовании контекста value должен быть мемоизирован, иначе обновления произойдут даже без изменений внутри value.
6. Избегать вложенных обновлений в контексте
Если контекст содержит вложенный объект, то при изменении любого поля произойдёт обновление всех компонентов, использующих этот контекст. Лучше разносить на атомарные куски или использовать библиотеки с подписками на части состояния (например, Zustand, Jotai).
7. Использование подписки на изменения вместо "наблюдения за всем"
Библиотеки, такие как Zustand, позволяют подписывать компоненты на конкретные фрагменты стора. Это предотвращает ненужные обновления:
const user = useStore(state => state.users\[123\]); // только при изменении пользователя с id=123
8. Использование React.memo с props от глобального состояния
Если компонент получает данные из глобального стора через props, и эти props не изменяются, то React.memo гарантирует, что компонент не перерендерится:
const TodoItem = React.memo(({ todo }) => {
return <li>{todo.title}</li>;
});
9. Разделение стора на атомы или независимые сущности
В Jotai или Recoil каждый кусок состояния может быть отдельным атомом. Тогда изменение одного атома не влияет на других.
const themeAtom = atom('dark');
const userAtom = atom({ id: 1, name: 'Alice' });
Каждый компонент подписывается только на нужный атом.
10. Изоляция async-запросов от рендера
Асинхронные запросы (fetch, axios) должны происходить вне рендера и желательно не триггерить ререндеры без необходимости.
-
Использовать React Query, SWR или селекторы.
-
Кэшировать запросы и использовать мемоизацию.
const { data: user } = useQuery(\['user', id\], fetchUser);
Особенность: изменения в других запросах не вызовут обновление этого компонента, если ключи разные.
Инструменты, способствующие оптимизации глобального состояния
-
Reselect (для Redux): мемоизированные селекторы, предотвращающие ререндер.
-
React.memo: предотвращает повторный рендер при неизменившихся props.
-
React.useMemo и useCallback: для передачи стабильно-ссылающихся объектов и функций.
-
Zustand с shallow: выборка нескольких полей без ререндера при неизменных значениях.
-
React Query: управление серверным состоянием без влияния на остальной UI.
-
Jotai/Recoil: атомарная модель глобального состояния.
-
Immer: упрощает иммутабельность без глубокого копирования руками.
-
DevTools (Redux DevTools, Zustand Devtools): отслеживание, какие действия вызвали ререндер и как менялось состояние.
Пример неправильного подхода
const GlobalContext = createContext();
const App = () => {
const \[state, setState\] = useState({ theme: 'dark', user: {...}, todos: \[...\] });
return <GlobalContext.Provider value={{ state, setState }}>...</GlobalContext.Provider>;
};
Любое изменение в state вызовет ререндер всех компонентов, даже если они используют только state.theme.
Пример правильного подхода с разделением
const ThemeContext = createContext();
const UserContext = createContext();
const App = () => {
const \[theme, setTheme\] = useState('dark');
const \[user, setUser\] = useState(null);
const themeValue = useMemo(() => ({ theme, setTheme }), \[theme\]);
const userValue = useMemo(() => ({ user, setUser }), \[user\]);
return (
<ThemeContext.Provider value={themeValue}>
<UserContext.Provider value={userValue}>
...
</UserContext.Provider>
</ThemeContext.Provider>
);
};
Каждый контекст изменяется независимо, что значительно снижает количество перерисовок.