LoginSignup
1
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-05

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

コード

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;
}

使い方

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

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

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById("root")
);

ステートを共有したい範囲の最上位コンポーネントを StoreProvider で括ります。
上記の例ではトップレベルに配置していますが、そこである必要はなく、もっと範囲を絞ることもできます。

useSelector

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

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

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

useDispatch

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1