11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ReactAdvent Calendar 2021

Day 4

useReducerとuseContextでglobal stateを作る場合のおすすめなやり方

Last updated at Posted at 2021-12-04

はじめに

React Contextでglobal stateを実現する際、useStateやuseReducerを使ってcontextの伝搬で更新するやり方が知られていますが、contextを一つだけにすると、無駄なレンダリングが生じることがあります。そこで、contextの伝搬を使う場合は、contextを細かく分けることがおすすめです。

コード

最初にuseReducerでstateを作るカスタムフックを作ります。

const useValue = () => useReducer((state, action) => {
  if (action.type === 'INC_COUNT1') {
    return { ...state, count1: state.count1 + 1 };
  }
  if (action.type === 'INC_COUNT2') {
    return { ...state, count2: state.count2 + 1 };
  }
  if (action.type === 'INC_BOTH_COUNTS') {
    return { ...state, count1: state.count1 + 1, count2: state.count2 + 1 };
  }
  throw new Error('unknown action type: ' + action.type)
}, { count1: 0, count2: 0 })

次にそれぞれの項目を伝搬させるためのcontextを作ります。

const DispatchContext = createContext(() => {});
const Count1Context = createContext(0);
const Count2Context = createContext(0);

Providerコンポーネントは次のようになります。入れ子にならないようにreduceRightを使います。

const Provider = ({ children }) => {
  const [{ count1, count2 }, dispatch] = useValue();
  return [
    [DispatchContext.Provider, { value: dispatch }],
    [Count1Context.Provider, { value: count1 }],
    [Count2Context.Provider, { value: count2 }],
  ].reduceRight(
    (a, c) => createElement(c[0], c[1], a),
    children
  );
};

実際にstateを使う2つのコンポーネントがこちらです。2つともほぼ同じです。

const Counter1 = () => {
  const count1 = useContext(Count1Context);
  const dispatch = useContext(DispatchContext);
  const inc = () => dispatch({ type: 'INC_COUNT1' });
  return (
    <div>
      Count1: {count1} <button onClick={inc}>+1</button>
    </div>
  );
};

const Counter2 = () => {
  const count2 = useContext(Count2Context);
  const dispatch = useContext(DispatchContext);
  const inc = () => dispatch({ type: 'INC_COUNT2' });
  return (
    <div>
      Count2: {count2} <button onClick={inc}>+1</button>
    </div>
  );
};

最後に全体をまとめるコンポーネントです。

const App = () => (
  <Provider>
    <div>
      <Counter1 />
      <Counter1 />
      <Counter2 />
      <Counter2 />
    </div>
  </Provider>
);

CodeSandbox

https://codesandbox.io/s/usereducer-usecontext-6okyp

課題

今回はシンプルな例でしたが、オブジェクトの構造が複雑でも、必要な数だけcontextを作ればなんとかなります。しかし、contextは動的には作れない(作ることはできるが、全部再マウントすることになってしまう)ので、あらかじめ分かっている固定数のものにしか対応できません。

おわりに

これをすんなり使おうと思える方はぜひどうぞ。こんなことするくらいならライブラリを使おうと思う方は、これとかこれとかこれとかをどうぞ。

11
9
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
11
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?