Як оптимізувати роботу з контекстом?
1) Діліть контекст на дрібні
Не кладіть "все підряд" в один Provider. Розбийте за доменами - тоді змінюється одне значення -> ререндеряться тільки його споживачі.
<UserProvider value={user}>
<ThemeProvider value={theme}>
<I18nProvider value={i18n}>
<App />
</I18nProvider>
</ThemeProvider>
</UserProvider>2) Мемоізуйте value
Головне правило: посилання value має змінюватися тільки коли змінюються його дані.
function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light'|'dark'>('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]); // добре
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}Анти-приклад (кожен рендер створює новий об'єкт -> усі споживачі смикаються):
<ThemeContext.Provider value={{ theme, setTheme }}> {/* погано */}3) Розносьте "часто змінюється" і "рідко змінюється"
Тримайте швидко змінюване окремо від стабільного.
const CounterValueContext = createContext<number>(0);
const CounterActionsContext = createContext<{ inc: () => void }>({ inc: () => {} });
function CounterProvider({ children }) {
const [count, setCount] = useState(0);
const inc = useCallback(() => setCount(c => c + 1), []);
return (
<CounterActionsContext.Provider value={{ inc }}>
<CounterValueContext.Provider value={count}>
{children}
</CounterValueContext.Provider>
</CounterActionsContext.Provider>
);
}Тепер зміна count не змінює посилання на actions, і споживачі дій не ререндеряться.
4) Тримайте Provider якомога ближче до споживачів
Ставте Provider навколо тієї частини дерева, де реально потрібен контекст - менше "підписників" -> менше ререндерів.
5) Використовуйте селектори (точкове читання)
З коробки React перемалює всіх consumers, але можна звузити оновлення бібліотеками контекст-селекторів:
use-context-selector- Zustand/Jotai/Redux Toolkit з
useSelector
Приклад з use-context-selector:
import { createContext, useContextSelector } from 'use-context-selector';
const CartContext = createContext<{items: Item[]; total: number}>({items:[], total:0});
function TotalPrice() {
const total = useContextSelector(CartContext, v => v.total); // ререндер тільки при зміні total
return <span>{total}</span>;
}6) Не передавайте "свіжостворені" об'єкти/масиви
Якщо потрібно віддавати похідні дані - мемоізуйте їх у споживача:
function ProductsList() {
const { items } = useContext(ProductsContext);
const visible = useMemo(() => items.filter(p => p.visible), [items]);
return <List items={visible} />;
}7) Для глобального змінюваного стану - useReducer
dispatch стабільний за посиланням -> зручно передавати через контекст.
const StoreContext = createContext<{state: State; dispatch: React.Dispatch<Action>}>(null!);
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initial);
const value = useMemo(() => ({ state, dispatch }), [state]); // dispatch стабільний
return <StoreContext.Provider value={value}>{children}</StoreContext.Provider>;
}8) Зовнішні стори: useSyncExternalStore
Якщо потрібна тонка підписка без ререндеру всього контексту, винесіть дані у зовнішній стор і підпишіться:
const subscribe = (listener: () => void) => store.subscribe(listener);
const getSnapshot = () => store.getState().pieceYouNeed;
const usePiece = () => useSyncExternalStore(subscribe, getSnapshot);Це дає "селективні" оновлення з коробки.
9) Стабілізуйте функції в value
Якщо в value є функції - обгортайте їх у useCallback, щоб не змінювати посилання даремно.
const actions = useMemo(() => ({ save, remove }), [save, remove]); // де save/remove - useCallback10) Контекст - для "конфігу", не для "лічильників"
Чим рідше змінюється контекст - тим краще. Часто оновлювані шматки стану тримайте локально (useState у компонентів) або у зовнішньому сторі; контекст - для конфігурації, тематики, прав, локалі, DI сервісів тощо.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.