Я знаю, не совсем оригинальная концепция. Но, услышав, как кто-то сказал, что они делают именно это в рабочем приложении, я подумал, что стоит изучить, что здесь задействовано и какие могут быть недостатки. Итак, без лишних слов, давайте приступим к делу.

Во-первых, давайте создадим контекст для простого счетчика, а вместе с ним и редюсер для его обновления. Затем мы можем поместить значение и связанные с ним действия в провайдер контекста, чтобы их можно было передать нашим компонентам:

import React, { createContext, useCallback, useReducer } from "react";
const initialState = 0;
export const CounterContext = createContext({
  counter: 0
});
function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    case "reset":
      return initialState;
    default:
      throw new Error();
  }
}
export const CounterProvider = (props) => {
  const [counter, dispatch] = useReducer(reducer, initialState);
const increment = useCallback(() => dispatch({ type: "increment" }), []);
  const decrement = useCallback(() => dispatch({ type: "decrement" }), []);
  const reset = useCallback(() => dispatch({ type: "reset" }), []);
  
  return (
    <CounterContext.Provider value={{
      counter,
      increment,
      decrement,
      reset
    }}>
      {props.children}
    </CounterContext.Provider>
  );
};

Достаточно просто. Теперь, поскольку это будет похоже на избыточность, нам нужно обернуть его вокруг нашего корневого компонента. Обычно с провайдерами это делается с помощью JSX следующим образом:

<CounterProvider>
  <App/>
</CounterProvider>

Но проблема в том, что обычно в приложениях Redux у вас есть много редьюсеров, и поэтому способ JSX может в конечном итоге включать множество вложенных провайдеров, обертывающих компонент приложения, т.е.

<CounterProvider>
  <OtherProvider1>
    <OtherProvider2>
      <OtherProvider3>
        <OtherProvider4>
          …

Хотя это сработает, это не очень эргономично. Поэтому мы можем использовать маленькую вспомогательную функцию, которая сделает за нас упаковку:

export default function Compose({ components = [], children }) {
  return (
    <>
      {components.reduceRight((acc, Comp) => {
        return <Comp>{acc}</Comp>
      }, children)}
    </>
  )
}

Теперь, когда мы визуализируем наше приложение в DOM, мы можем просто обернуть его в этот компонент Compose и предоставить массив провайдеров:

<Compose components={[CounterProvider]}>
  <App />
</Compose>

Таким образом, каждый раз, когда вы хотите добавить еще одного провайдера, вы просто добавляете его в массив. Красиво и чисто. Теперь все, что осталось сделать, это создать простой компонент, подключенный к этому «магазину», заставив его использовать контекст.

function Counter() {
  const {
    counter,
    increment,
    decrement,
    reset
  } = useContext(CounterContext);
  return (
    <div>
      {counter}
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

Все сделано! Компонент Counter имеет доступ к значению счетчика, а также к действиям для отправки. Несколько компонентов счетчика также будут иметь одни и те же значения контекста, как и несколько компонентов, подключенных к хранилищу Redux.

Но что, если мы хотим, чтобы компонент мог обновлять контекст, но не был привязан к значению? Значит, он не перерисовывается при изменении значения контекста? Например, компонент кнопки, который увеличивает счетчик, но на самом деле не нуждается в самом значении счетчика. Что ж, есть простой способ сделать это.

Во-первых, мы создаем файл dispatcher.js, который просто экспортирует пустой объект по умолчанию. Затем, когда мы создаем наши контексты, мы можем назначить функции отправки объекту как свойства. Вот так:

import dispatcher from "dispatcher";
. . .
export const CounterProvider = (props) => {
  const [counter, dispatch] = useReducer(reducer, initialState);
const increment = useCallback(() => dispatch({ type: "increment" }), []);
  const decrement = useCallback(() => dispatch({ type: "decrement" }), []);
  const reset = useCallback(() => dispatch({ type: "reset" }), []);
if (!dispatcher.CounterDispatcher) {
    dispatcher.CounterDispatcher = {
      increment,
      decrement,
      reset,
    };
  }
  
  return (
    <CounterContext.Provider value={{
      counter,
      increment,
      decrement,
      reset
    }}>
      {props.children}
    </CounterContext.Provider>
  );
};

Теперь этот объект диспетчера имеет функции для обновления контекста! Давайте используем их в другом компоненте:

import dispatcher from "dispatcher";
function IncrementButton() {
    return <button onClick={dispatcher.CounterDispatcher.increment}>Increment</button>
};
export default IncrementButton;

Теперь компонент может без разбора обновлять контекст счетчика, фактически не «используя» контекст и не привязываясь к его значению.

Недостатки

Итак, теперь, когда мы воссоздали некоторые базовые Redux-подобные функции с хуками и контекстом, каковы ограничения?

  1. Нет собственных селекторов — когда вы используете контекст, вы получаете все содержащиеся в нем данные, и если любое из этих значений изменится, компоненты, использующие его, будут перерисованы. Это нормально в нашем небольшом примере со счетчиком только с одним значением, но когда вы сохраняете более сложные вложенные данные в контексте, то обновление одного конкретного значения вызовет повторную визуализацию КАЖДОГО компонента с использованием контекста, независимо от того, зависит ли компонент от него. это конкретное значение. Это может ухудшить производительность. Надеюсь, скоро появится встроенная поддержка селекторов, а пока такие библиотеки, как эта, стремятся добавить эту функциональность.
  2. Нет промежуточного программного обеспечения — в настоящее время нет собственного способа добавления промежуточного программного обеспечения к редюсерам и нет простого способа применить одно промежуточное программное обеспечение ко всем редьюсерам во всех контекстах.
  3. Никаких инструментов разработки — инструменты разработки Redux очень ценны для просмотра того, как выглядит ваш магазин и что с ним происходит. На данный момент нет такого основного инструмента для контекстов, но утилиты, служащие для этой цели, скорее всего, скоро появятся. Инструменты разработчика React позволяют просматривать текущее состояние провайдера, хотя вам нужно найти его в дереве компонентов, что не так удобно для пользователя, а также не показывает отправляемые действия или результирующее отличие от действия. был.

Если вы заменили Redux на хуки+контекст в своих приложениях, я был бы рад услышать ваши мысли о том, как это было, какие преимущества они вам предоставили и как вы справлялись с недостатками или устраняли их.