Урок 05

Контекст: продвинуто

Проблема ререндеров

Когда значение контекста — объект, создаваемый в провайдере, он меняется при каждом рендере провайдера. Все потребители перерисовываются без причины.

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
  • Разделяй контексты по смыслу
  • Не клади в контекст часто меняющиеся данные
  • Контекст + редьюсер — удобный паттерн
  • Для глобального состояния лучше стор