Глобальний стейт і продуктивність
Що таке "глобальний стан"
Глобальний стан - це дані, до яких можуть звертатися різні компоненти з різних частин застосунку, наприклад:
- поточний користувач,
- тема (dark/light),
- авторизація,
- кошик товарів,
- налаштування застосунку тощо.
Такі дані зазвичай зберігаються в:
- React Context API (
useContext); - Redux / Zustand / Jotai / MobX;
- глобальних store-синглтонах.
Проблема: надлишковий глобальний стан
Коли розробник починає класти все підряд у глобальний store, навіть те, що потрібно лише одному компоненту, виникає ланцюгова реакція перерендерів і ускладнення застосунку.
Чому це шкідливо для продуктивності
1. Кожне оновлення глобального стану викликає масові перерендери
React за замовчуванням сповіщає всіх споживачів контексту (або store), які його читають, навіть якщо вони не використовують змінене поле.
const ThemeContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
<Footer />
</ThemeContext.Provider>
);
}Якщо ти зміниш theme,
→ всі компоненти, що використовують useContext(ThemeContext),
будуть перерендерені,
навіть якщо вони не відображають тему напряму.
Чим більше компонентів прив'язано до контексту, тим більше непотрібних оновлень.
2. Втрачається "локалізація" стану
Якщо все зберігається в одному великому store, будь-який апдейт → тригерить підписників по всьому дереву.
Наприклад, зміна "пошукового запиту" в списку товарів не повинна чіпати "панель профілю користувача", але якщо все в одному Redux-слайсі, обидва перерахуються.
3. Неможливо ефективно мемоізувати
Коли дані глобальні, складно ізолювати компонент:
const user = useSelector((state) => state.user);→ При будь-якій зміні в state.user React-Redux тригерить перерендер,
навіть якщо компонент використовує лише user.name.
Якщо таких "дрібних" підписок десятки, починається "каскад перерендерів".
4. Контексти не вміють часткових оновлень
React Context не підтримує дифф за значеннями.
Якщо у value хоч щось змінилося,
всі useContext-споживачі отримують новий об'єкт → перерендер.
<Provider value={{ theme, user }}>...</Provider>→ навіть якщо змінився user, компонент, що використовує лише theme,
все одно перемалюється, тому що об'єкт { theme, user } новий за посиланням.
5. Зростає когнітивне навантаження
Чим більше глобального стану, тим важче зрозуміти:
- що де зберігається;
- хто оновлює;
- які компоненти залежать від чого.
Це призводить до непередбачуваних побічних ефектів, і оптимізація стає складнішою, ніж сама бізнес-логіка.
6. Проблеми з конкурентним рендерингом
У React 18 при увімкненому concurrent mode React може:
- призупинити рендер;
- відкотити зміни;
- перерендерити частину дерева.
Великий глобальний store робить це дорожче, тому що React змушений копіювати і тримати знімки більшого обсягу даних.
Приклад (наочно)
// все в глобальному контексті - погана практика
const GlobalContext = createContext();
function App() {
const [theme, setTheme] = useState('light');
const [cart, setCart] = useState([]);
const [search, setSearch] = useState('');
const value = { theme, cart, search, setTheme, setCart, setSearch };
return (
<GlobalContext.Provider value={value}>
<Header /> // використовує лише theme
<Search /> // використовує лише search
<Cart /> // використовує лише cart
</GlobalContext.Provider>
);
}Якщо змінити search,
- Header і Cart все одно перерендеряться,
тому що
value(об'єкт) змінився за посиланням.
Рішення (як зробити правильно)
1. Розділяй контексти / стора Створюй окремі провайдери для незалежних частин:
<ThemeProvider>
<CartProvider>
<SearchProvider>
<App />
</SearchProvider>
</CartProvider>
</ThemeProvider>Кожен контекст оновлюється ізольовано.
2. Тримай локальний стан локальним
Якщо дані потрібні лише одному компоненту (або його дітям),
використовуй useState прямо в ньому, а не в Redux/Context.
function SearchBar() {
const [query, setQuery] = useState('');
...
}3. Роби селективні підписки Якщо використовуєш Redux / Zustand, використовуй селектори і shallow порівняння, щоб підписуватися лише на потрібні поля.
const price = useSelector(state => state.cart.totalPrice);→ Зміниться лише якщо totalPrice зміниться.
4. Використовуй мемоізацію і split-компоненти
- Обгортай важкі компоненти в
React.memo; - Передавай обробники через
useCallback; - Дроби компонент так, щоб глобальні дані не тягли все дерево.
5. Для складних станів - useContextSelector (або Jotai / Zustand)
React 19 додасть Context Selectors нативно.
Поки можна використовувати use-context-selector:
вона дозволяє підписуватися на конкретне значення в контексті, а не на весь об'єкт.
Підсумок
| Причина | Чому шкідливо |
|---|---|
| Масові перерендери | Усі підписники оновлюються навіть при дрібній зміні |
| Погана ізоляція | Будь-яка зміна тригерить все дерево |
| Важкі знімки | Збільшує пам'ять і роботу GC |
| Складно зрозуміти залежності | Зростає ймовірність багів |
| Неможлива точкова оптимізація | Context не робить дифф за полями |
Правильне мислення:
"Зберігай мінімально необхідне в глобальному стані. Все інше тримай локально, якомога ближче до місця використання."
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.