React Context API: Глубокое погружение для senior-разработчиков

#react#context-api#state-management

TL;DR

Context API — это React-native решение для глобального state management без prop drilling. Разбираемся, когда его использовать, как избежать лишних ререндеров и какие есть альтернативы для сложных сценариев.

Введение: Context API в 2024

Несмотря на появление новых state management решений (Jotai, Zustand), Context API остается базовым инструментом в арсенале React-разработчика. Но многие senior-инженеры до сих пор используют его неправильно — либо как молоток для всех задач, либо избегают из-за мифов о производительности.

Основная часть: Beyond Basics

1. Оптимизация ререндеров

Главная проблема Context — все потребители (consumers) перерисовываются при изменении значения. Решение — разделение контекстов и мемоизация:

// Плохо: один контекст для всего
const AppContext = createContext({
  user: null,
  theme: 'light',
  notifications: []
});

// Хорошо: split contexts
const UserContext = createContext(null);
const ThemeContext = createContext('light');
const NotificationsContext = createContext([]);

2. Паттерн “Context + useReducer”

Для сложной бизнес-логики вместо useState лучше использовать useReducer:

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);
  
  return (
    <CartContext.Provider value={state}>
      <CartDispatchContext.Provider value={dispatch}>
        {children}
      </CartDispatchContext.Provider>
    </CartContext.Provider>
  );
}

// Потребление с оптимизацией
function AddToCartButton({ productId }) {
  const dispatch = useContext(CartDispatchContext);
  
  return (
    <button onClick={() => dispatch({ type: 'ADD', productId })}>
      Add to Cart
    </button>
  );
}

3. TypeScript и Context

Продвинутая типизация для контекста с гарантией non-null значений:

type ThemeContextType = {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
};

// Фабрика для безопасного контекста
function createStrictContext<T>() {
  const context = createContext<T | null>(null);
  
  const useStrictContext = () => {
    const ctx = useContext(context);
    if (!ctx) throw new Error('Context used without Provider');
    return ctx;
  };
  
  return [context, useStrictContext] as const;
}

const [ThemeContext, useTheme] = createStrictContext<ThemeContextType>();

Практическое применение: Real-world примеры

1. Feature Flags

const FeatureFlagsContext = createContext<Record<string, boolean>>({});

export const FeatureFlagsProvider = ({
  flags,
  children,
}: {
  flags: Record<string, boolean>;
  children: ReactNode;
}) => {
  const value = useMemo(() => flags, [flags]);
  
  return (
    <FeatureFlagsContext.Provider value={value}>
      {children}
    </FeatureFlagsContext.Provider>
  );
};

// Потребление с кастомным хуком
export const useFeatureFlag = (flag: string) => {
  const flags = useContext(FeatureFlagsContext);
  return flags[flag] ?? false;
};

2. Performance-sensitive контексты

Для часто изменяющихся данных (например, положение курсора):

const MousePositionContext = createContext({ x: 0, y: 0 });

export const MousePositionProvider = ({ children }) => {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  
  useEffect(() => {
    const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handler);
    return () => window.removeEventListener('mousemove', handler);
  }, []);

  // Оптимизация: стабильная ссылка на value
  const value = useMemo(() => pos, [pos.x, pos.y]);
  
  return (
    <MousePositionContext.Provider value={value}>
      {children}
    </MousePositionContext.Provider>
  );
};

// Оптимизированный consumer
function CursorTracker() {
  const { x, y } = useContext(MousePositionContext);
  return <div style={{ position: 'fixed', left: x, top: y }}>•</div>;
}

Когда НЕ использовать Context API

  1. Для высокочастотных обновлений (анимации, реальное время)
  2. Когда нужны селекторы (как в Redux)
  3. Для shared state между несвязанными частями приложения
  4. Когда нужны middleware (логирование, persistance)

Для этих случаев рассмотрите:

Заключение

Context API — это не “упрощенный Redux”, а инструмент для композиции. Правильно используя разделение контекстов, мемоизацию и TypeScript, можно создавать производительные и поддерживаемые решения. Для сложных сценариев комбинируйте Context с другими state management подходами.


Источник: https://dev.to/m_saad_ahmad/day-9-of-100-days-of-code-understanding-the-react-context-api-2198