Оптимізація контексту
1) Ділиш контекст на смислові частини
Не клади все в один "товстий" value.
- Було:
AppContext = { user, theme, notifications, cart } - Стало:
AuthContext,ThemeContext,UIContext,CartContext
Так зміни в кошику не тригерять перемальовування підписників теми і т.д.
2) Стабілізуй value у Provider
Ререндер усіх підписників спрацьовує при зміні посилання. Дай стабільні посилання:
function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
// функції теж стабілізуємо
const login = useCallback((u: User) => setUser(u), []);
const logout = useCallback(() => setUser(null), []);
// value змінюється тільки коли змінюється user
const value = useMemo(() => ({ user, login, logout }), [user]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}Уникай:
<AuthContext.Provider value={{ user, login: (u)=>setUser(u) }}>(кожен ререндер створює новий об'єкт/функції -> усі підписники оновляться).
3) Розділяй "стан" і "диспатч" на два контексти
dispatch/екшени стабільно-посилальні, їх можна віддавати окремим провайдером:
const CounterStateContext = createContext<number>(0);
const CounterDispatchContext = createContext<React.Dispatch<Action>>(() => {});
function CounterProvider({ children }) {
const [count, dispatch] = useReducer(reducer, 0);
return (
<CounterDispatchContext.Provider value={dispatch}>
<CounterStateContext.Provider value={count}>
{children}
</CounterStateContext.Provider>
</CounterDispatchContext.Provider>
);
}
// Підписник, якому потрібна лише відправка дій, не ререндериться при зміні count4) Став Provider якомога нижче в дереві
Чим менше підписників усередині області дії Provider, тим менший масштаб оновлень.
- Обгортай потрібні гілки, а не весь
<App />.
5) Використовуй селектори (точкові підписки)
Бібліотека use-context-selector дозволяє підписуватися на частину value, а не на весь об'єкт.
import { createContext, useContextSelector } from 'use-context-selector';
const UserContext = createContext<{user: User, setUser:Fn}>(/* ... */);
function UserName() {
const name = useContextSelector(UserContext, v => v.user.name);
return <span>{name}</span>; // Ререндериться тільки якщо змінилося ім'я
}Це дає гранулярні оновлення як у Redux.
6) Обгортки-селектори + React.memo
Іноді зручно зробити маленький компонент-селектор, який читає контекст і віддає рівно потрібний проп вниз мемоізованому компоненту.
const PriceView = React.memo(function PriceView({ price }: { price: number }) {
return <span>{price}</span>;
});
function PriceFromContext() {
const price = useContext(CartContext).totalPrice; // тільки одне поле
return <PriceView price={price} />;
}React.memo тут "відрізає" зайві ререндери нижче, якщо проп не змінився.
7) Не зберігай у контексті "товсті"/часто змінювані структури
- Клади ідентифікатори і прості дані.
- Важкі колекції/кеш тримай у зовнішньому сторі (
useSyncExternalStore, Zustand/Jotai/Redux) або локально на місці.
8) Для частих оновлень - краще зовнішній стор
Context не про high-frequency оновлення (таймери, курсор, введення). Для таких задач:
- Zustand/Jotai/Redux + селектори
useSyncExternalStoreіз власною шиною підписок Вони оновлюють лише тих, хто реально читає частину, що змінилася.
9) Часті дрібниці, які рятують
- Не передавай у
valueнові масиви/об'єкти безuseMemo. - Екшени/колбеки - через
useCallback. - У
Providerне обчислюй важке на льоту - мемоізуй або виноси. - У TypeScript можна типізувати контексти окремо:
StateіActions- менше спокуси "склеїти все в одне".
Коротка "шпаргалка" вибору
- Рідкі оновлення + прості дані -> Context (із п.1-6 вище).
- Багато різних полів, що часто змінюються -> Розділити контексти або перейти на стор із селекторами.
- Дуже часті оновлення (введення, курсор, розміри вікна) -> зовнішній стор/підписка (
useSyncExternalStore, Zustand тощо).
Якщо хочеш, накину міні-рефактор: надішли свій поточний Provider/підписників - перетворю на варіант із розділеними контекстами і селекторами.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.