Skip to main content

Як типізувати контекст?

1) Базовий і безпечний патерн (з undefined + кастомний хук)

Коли контекст обов'язковий усередині піддерева і не повинен бути null/undefined під час використання.

javascript
import React, { createContext, useContext, ReactNode } from "react"; type Theme = "light" | "dark"; type ThemeCtx = { theme: Theme; setTheme: (t: Theme) => void; }; // 1) Контекст допускає undefined до Provider const ThemeContext = createContext<ThemeCtx | undefined>(undefined); // 2) Безпечний хук: кидає зрозумілу помилку, якщо провайдера немає export function useTheme() { const ctx = useContext(ThemeContext); if (!ctx) throw new Error("useTheme must be used within <ThemeProvider>"); return ctx; } // 3) Провайдер із типами пропсів export function ThemeProvider({ value, children, }: { value: ThemeCtx; children: ReactNode; }) { return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; }

Чому так краще:

  • не потрібно писати !/кастувати as ThemeCtx;
  • отримуємо ранню і зрозумілу помилку, якщо забули обгорнути в провайдер.

2) Контекст із валідним дефолтом (якщо він реально є)

Якщо поза провайдером значення осмислене (наприклад, read-only налаштування).

javascript
type Config = { apiBase: string; locale: string }; const defaultConfig: Config = { apiBase: "/api", locale: "en" }; const ConfigContext = createContext<Config>(defaultConfig); export const useConfig = () => useContext(ConfigContext);

Важливо: давай дефолт тільки тоді, коли він справді коректний для runtime, інакше ти приховаєш баг.


3) Контекст, який може бути null за доменом

Якщо «немає значення» - це нормальний кейс.

javascript
type User = { id: string; name: string } | null; const CurrentUserContext = createContext<User>(null); export const useCurrentUser = () => useContext(CurrentUserContext);

4) Пара «state + dispatch» (зручно для редьюсерів)

javascript
type Action = | { type: "set"; payload: number } | { type: "reset" }; type CounterState = { count: number }; type CounterCtx = { state: CounterState; dispatch: React.Dispatch<Action>; }; const CounterContext = createContext<CounterCtx | undefined>(undefined); export function useCounter() { const ctx = useContext(CounterContext); if (!ctx) throw new Error("useCounter must be used within <CounterProvider>"); return ctx; }

5) Кортеж/мульти-значення (іноді зручніше)

javascript
type Filters = { q: string; tags: string[] }; type SetFilters = (next: Filters) => void; const FiltersContext = createContext<[Filters, SetFilters] | undefined>(undefined); export function useFilters() { const ctx = useContext(FiltersContext); if (!ctx) throw new Error("useFilters must be used within <FiltersProvider>"); return ctx; // поверне [filters, setFilters] }

6) Типізація провайдера (пропси value і children)

javascript
type AuthCtx = { token: string | null; login: (t: string) => void }; const AuthContext = createContext<AuthCtx | undefined>(undefined); type AuthProviderProps = { value: AuthCtx; // строго типізуємо value children: ReactNode; }; export function AuthProvider({ value, children }: AuthProviderProps) { return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; }

7) Уникай анти-патернів

  • createContext<Ctx>(null as unknown as Ctx) чи null! - приховує помилки.
  • Давати «фіктивний» дефолт, якщо без провайдера працювати не можна.
  • Зберігати в контексті часто змінювані примітиви без потреби (спричинить зайві рендери).

8) Нюанси і поради

  • Для великих значень мемоізуй value у провайдері:

    javascript
    const value = useMemo(() => ({ theme, setTheme }), [theme]);
  • Для SSR/тестів зручно мати валідний дефолт (патерн №2) - якщо це справді безпечно.

  • Якщо контекст використовується в різних пакетах, експортуй і контекст, і хук, але споживачам віддавай саме хук - так ти нав'яжеш безпечне використання.


Коротка шпаргалка

  • Обов'язковий провайдер -> createContext<T | undefined>(undefined) + useXxx() з помилкою.
  • Є осмислений дефолт -> createContext<T>(defaultValue).
  • Допускається відсутність -> createContext<T | null>(null).
  • Reducer-стиль -> createContext<{ state: S; dispatch: Dispatch<A> } | undefined>(undefined).

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.