createContextとuseReducerで簡易的なReduxを再現する

`createContext` と `useReducer` を組み合わせて簡易的な Redux を再現する方法を記します。

# コード

```tsx
import React from "react";

type StoreProviderProps<Store> = {
  initialState?: Store;
};

export function createStore<Store, Action>(
  initialState: Store,
  reducer: React.Reducer<Store, Action>
) {
  const storeContext = React.createContext(initialState);
  const dispatchContext = React.createContext<React.Dispatch<Action>>(() => {});

  const StoreProvider: React.FC<StoreProviderProps<Store>> = (props) => {
    const [state, dispatch] = React.useReducer(
      reducer,
      props.initialState ?? initialState
    );

    return (
      <storeContext.Provider value={state}>
        <dispatchContext.Provider value={dispatch}>
          {props.children}
        </dispatchContext.Provider>
      </storeContext.Provider>
    );
  };

  function useSelector(): Store;
  function useSelector<T>(selector: (state: Store) => T): T;
  function useSelector<T>(selector?: (state: Store) => T) {
    const state = React.useContext(storeContext);

    return selector !== undefined ? selector(state) : state;
  }

  function useDispatch() {
    return React.useContext(dispatchContext);
  }

  return [StoreProvider, useSelector, useDispatch] as const;
}
```

# 使い方

```tsx
import { createStore } from "./createStore";

type State = { count: number };

type Action =
  | { type: "increment" }
  | { type: "incrementByAmount"; payload: number };

const initialState: State = { count: 2 };

const reducer: React.Reducer<State, Action> = (state, action) => {
  switch (action.type) {
    case "increment": {
      return { ...state, count: state.count + 1 };
    }
    case "incrementByAmount": {
      return { ...state, count: state.count + action.payload };
    }
  }
};

export const [StoreProvider, useSelector, useDispatch] = createStore(initialState, reducer);
```

`createStore`関数に初期ステートと`reducer`関数を渡します。
`reducer`関数は直前のステートとアクションを引数で受け取り新ステートを返すよう作ります。

この関数の戻り値は、順番に`StoreProvider`, `useSelector`, `useDispatch` となっています。(固定長タプルなので任意の名前をつけることができます。)

## `StoreProvider`

```tsx
function App() {
  return (
    <div>
      <Count />
      <Incrementer />
    </div>
  );
}

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById("root")
);
```
ステートを共有したい範囲の最上位コンポーネントを `StoreProvider` で括ります。
上記の例ではトップレベルに配置していますが、そこである必要はなく、もっと範囲を絞ることもできます。

## `useSelector`

```tsx
const Count = () => {
  const count = useSelector((state) => state.count);

  return <p>count: {count}</p>;
};
```

`StoreProvider` の内側のコンポーネントで `useSelector` フックを使用して現在のステートを取得することができます。
Reduxの `useSelctor` を意識して `(state) => state.count` のようなステートを変換する関数を受け取れるようにしてありますが、Reduxのようにそのコンポーネントで使用していないステートの変化による re-render を抑制する効果はありません。(あくまでインターフェイスの簡易的な再現)

## `useDispatch`

```tsx
const Incrementer = () => {
  console.log("rendered Incrementer.");
  const [amount, setAmount] = React.useState("1");
  const dispatch = useDispatch();

  return (
    <>
      <button onClick={() => dispatch({ type: "increment" })}>increment</button>
      <input
        type="number"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
      />
      <button
        onClick={() =>
          dispatch({
            type: "incrementByAmount",
            payload: Number(amount) || 0,
          })
        }
      >
        increment by amount
      </button>
    </>
  );
};
```

`StoreProvider` の内側のコンポーネントで `useDispatch` フックを使用して `dispatch` 関数を取得することができます。
クリックイベントなどで `dispatch` 関数に `reducer` 関数が要求している形式の `action` オブジェクトを渡してあげることでステートを更新することができます。
TypeScriptを使用していれば、 `Action` のタイプなりインターフェイスを定義して使用されるので、決められた形以外の `action` オブジェクトを `useDispatch` に渡そうとするとコンパイルエラーになります。また、エディターの補完も効くのでTypeScriptのほうが開発者体験がいいはずです。

# 説明

やっていることは `useReducer` のステートと `dispatch` をコンテキストで配信しているだけです。
`useContext` をラップしたフックを作ることでreduxのAPIを真似ています。

ただし `useContext` を使用するコンポーネントは、当該コンテキストの変更をすべて検知して re-render されてしまうので、更新頻度の高いステートを大きなコンポーネントツリーで共有するとパフォーマンスに影響します。

せめてものパフォ低下対策として、ステートと `dispatch` 関数を配信するコンテキストを分けることで、`useDispatch` だけを使用しているコンポーネントがステート更新によって re-render されないようにはなっています。

# 終わりに

わ た し は r e d u x を 使 っ て ア プ リ を 作 っ た こ と が な い 。

# 参考
https://ja.reactjs.org/docs/hooks-reference.html#usereducer
https://ja.reactjs.org/docs/hooks-reference.html#usecontext
https://mizchi.dev/202005271609-react-app-context