Контекст: продвинуто
Проблема ререндеров
Когда значение контекста — объект, создаваемый в провайдере, он меняется при каждом рендере провайдера. Все потребители перерисовываются без причины.
function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
// Новый объект каждый рендер
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Решение: useMemo
function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const login = useCallback((user: User) => setUser(user), []);
const logout = useCallback(() => setUser(null), []);
const value = useMemo(
() => ({ user, login, logout }),
[user, login, logout]
);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
Теперь объект меняется только при изменении user.
Разделение контекстов
Если в одном контексте и данные, и функции — потребители перерисовываются, даже если им нужна только часть. Разделяй:
const UserContext = createContext<User | null>(null);
const UserActionsContext = createContext<{
login: (u: User) => void;
logout: () => void;
} | null>(null);
Компонент, которому нужны только действия, не будет перерисовываться при изменении user.
Несколько контекстов
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const value = useMemo(
() => ({ theme, setTheme }),
[theme]
);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function App() {
return (
<AuthProvider>
<ThemeProvider>
<Layout />
</ThemeProvider>
</AuthProvider>
);
}
Значения по умолчанию для тестов
Контекст можно замокать в тестах:
render(
<AuthContext.Provider value={mockValue}>
<Component />
</AuthContext.Provider>
);
Контекст и SSR
На сервере каждый запрос должен создавать новый контекст, чтобы данные не утекали между пользователями.
В Next.js App Router провайдеры оборачивают в клиентский компонент:
'use client';
export function Providers({ children }) {
return <AuthProvider>{children}</AuthProvider>;
}
Паттерн: контекст + редьюсер
Сложное состояние удобно держать в контексте через useReducer:
function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, initialState);
const value = useMemo(() => ({ state, dispatch }), [state]);
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
Компоненты отправляют действия через dispatch.
Когда не использовать
- Данные нужны только на одном уровне — пропсы
- Данные меняются очень часто — стор (Zustand, Redux)
- Данные нужны одному компоненту — локальный стейт
Контекст — не замена стору. Он для данных, которые редко меняются и нужны многим компонентам.
Итоги
- Мемоизируй значение контекста через
useMemo - Разделяй контексты по смыслу
- Не клади в контекст часто меняющиеся данные
- Контекст + редьюсер — удобный паттерн
- Для глобального состояния лучше стор