Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

useReducerの活用法について

こんにちは、フロントエンドエンジニアの松尾です。

Reactにはフック(Hooks)と呼ばれる機能があり、useStateuseEffectuseCallbackといった様々な関数が用意されています。

その中の一つにuseReducerがありますが、useContextと併用して使うものとして認識されたり、useStateの影に隠れたりといった理由であまり活用されていない傾向があります。

しかし、useReducerは様々な場面で活用でき、可読性・パフォーマンス向上に大いに役立ちます。

本記事では、useReducerの基本的な使い方と様々な活用法について説明したいと思います。

useReducerとは?

useReducerとは、useStateと同様に状態管理ができるフックです。

特に複雑なロジックが絡んだ状態を管理するのに適しています。

useReducerは以下のように宣言することで使用できます。

const [state, dispatch] = useReducer(reducer, initialState)

必要な引数は、reducerinitialStateです。

  • reducer:stateとactionを受け取り、新しいstateを返す関数

  • initialState:stateの初期値

戻り値としてstatedispatchを返します。

  • state : 現在のstate値

  • dispatch : reducerを実行するための関数

では実際に、useReducerの使用例を見てみましょう。以下はReact公式ドキュメントから引用したコードになります。

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

initialStateでカウント値の初期値を定義しており、reducerにてincrementdecrementのアクションを用意しています。

処理の流れは以下の通りです。

  1. ボタンを押すとdispatchが実行され、中身がreducerのactionに入る。

  2. switch文によって処理を分岐させ、新しいstate値を計算して返す。

  3. useReducerが更新されたstate値を受け取り、新しいstate値を描画させる。

reduxの書き方と非常に似ていますが、useReducerはdispatchにオブジェクトを返す必要はなく、値をそのまま入れることも可能です。

const initialState = {count: 0};

function reducer(state, action) {
  return state + action;
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch(-1)}>-</button>
      <button onClick={() => dispatch(+1)}>+</button>
    </>
  );
}

このようにシンプルに記述できることもuseReducerの魅力の一つです。

ここまでの説明で「useStateでもよくない?」と思われた方も多いのではないでしょうか?

ここからは、useReducerならではの強みについて説明したいと思います。

状態管理をuseReducerにまとめられる

useStateで状態管理していると、宣言が多すぎて複雑になってしまうことはありませんか?

例えば以下のような場合です。

const App = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [type, setType] = useState('small')
  const [phone, setPhone] = useState('')
  const [email, setEmail] = useState('')
  const [error, setError] = useSatte(null)

  return (
    ...
  )
}

このように状態管理が多い場合、ロジックが複雑になるにつれ可読性が悪化してしまうケースが多々あります。

状態管理をuseReducerにまとめることで「何を状態管理しているか」「初期値はなんなのか」が明確になります。

const initialState = {
  isOpen: false,
  type: 'small',
  phone: '',
  email: '',
  error: null
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
    default:
      return state
  }
}

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    ...
  )
}

ロジックとビューを切り離すことができる

useReducerのもう一つの強みは、ロジックとビューを切り離すことができる点です。

今回サンプルとして、以下を実装します。

3つのitemがあり、それぞれの値の更新・削除ができるといったシンプルな実装です。

上記をuseStateuseReducerそれぞれで実装し見比べてみましょう。

まずuseStateの場合です。

const App = () => {
  const [items, setItems] = useState([0, 0, 0]);

  const increment = (index) => {
    const newItems = items.slice()
    newItems[index]++
    setItems(newItems)
  }

  const decrement = (index) => {
    const newItems = items.slice()
    newItems[index]--
    setItems(newItems)
  }

  const deleteItem = (index) => {
    setItems(items.filter((_, i) => i !== index))
  }

  return (
    <>
      {
        items.map((v, i) => (
          <div key={i}>
            item{i}: {v}
            <button onClick={() => increment(i)}>+</button>
            <button onClick={() => decrement(i)}>-</button>
            <button onClick={() => deleteItem(i)}>削除</button>
          </div>
        ))
      }
    </>
  );
}

このように、useStateの場合は、ロジックとビューが混ざった状態となっています。

この程度の規模であれば問題ないかと思いますが、もしロジックが複雑になってきた場合、どの関数がどの状態を更新しているのかが分かりづらくなってしまいます。

次に、useReducerの場合です。

const myReducer = (state, action) => {
  const newState = state.slice()
  switch (action.type) {
    case 'increment':
      newState[action.index]++
      return newState
    case 'decrement':
      newState[action.index]--
      return newState
    case 'delete':
      return state.filter((_, i) => i !== action.index);
    default:
      throw new Error();
  }
};

const App = () => {
  const [items, dispatch] = useReducer(myReducer, [0, 0, 0]);

  return (
    <>
      {
        items.map((v, i) => (
          <div key={i}>
            item{i}: {v}
            <button onClick={() => dispatch({type: 'increment', index: i})}>
              +
            </button>
            <button onClick={() => dispatch({type: 'decrement', index: i})}>
              -
            </button>
            <button onClick={() => dispatch({type: 'delete', index: i})}>
              削除
            </button>
          </div>
        ))
      }
    </>
  );
}

このように、useReducer内にロジック処理を記述できるため、ビューと完全に切り離すことができます。

もし機能追加となった場合も、わざわざ関数を用意せずに、reducerのswitch文に新しく処理を追加するだけで済みます。

また、useReducerを用いるとdispatch関数はメモ化されるため、もし返ってくる値が変わらない場合はコンポーネントの再レンダリングを防ぐこともできます。

以上のことから、ロジックが複雑な処理に関しては、なるべくuseReducerを使用することをおすすめします。

まとめ

以上useReducerの基本的な使い方と活用法についての説明でした。

useReducerは、useStateと比べると使い方が馴染みづらく避けがちですが、活用することで様々な処理の可読性・パフォーマンス向上に役立ちます。

是非今回紹介した方法をお試し下さい!

最後になりますが、Wizではエンジニアを募集中です。

興味のある方は是非覗いてみてください↓

careers.012grp.co.jp