React useReducerフック

概要: このチュートリアルでは、複雑な状態ロジックを処理するためにReactのuseReducerフックを使用する方法を学びます。

React useReducer()フックの紹介

useReducerフックは、useStateフックの代替であり、コンポーネントの状態を管理できます。

  • useReducerフックは状態を生成します。
  • 状態を変更すると、コンポーネントの再レンダリングがトリガーされます。

useReducerフックは、次のような場合に役立ちます。

  • コンポーネントに、互いに密接に関連する複数の状態がある場合。
  • 将来の状態の値が、現在の状態に依存する場合。

useReducerフックを使用するには、次の手順に従います。

ステップ1. reactライブラリからuseReducerフックをインポートします

import { useReducer } from 'react';Code language: JavaScript (javascript)

ステップ2. stateactionの2つのパラメータを受け取るreducer()関数を定義します

function reducer(state, action) {
  // ...
}Code language: JavaScript (javascript)

ステップ3. コンポーネントでuseReducerフックを使用します

function MyComponent() {
   const [state, dispatch] = useReducer(reducer, initialArg, init?);
   // ...
}Code language: JavaScript (javascript)

以下は、useReducerフックの構文です。

  • reducerは、状態がどのように更新されるかを決定する関数です。
  • initialArgは初期状態です。
  • initは、初期状態を返す関数を指定するオプションのパラメータです。省略した場合、初期状態はinitialArgに設定されます。それ以外の場合、初期状態はinit(initialArg)の呼び出し結果に設定されます。

useReducer()関数は、正確に2つの値を持つ配列を返します。

  • stateは現在の状態です。
  • dispatch関数を使用すると、状態を新しい状態に更新し、再レンダリングをトリガーできます。

以下は、useReducerフックの動作を示しています。

useReducer

React useReducer()フックの例

以下は、カウンターアプリを示しています。

インクリメントボタンをクリックすると、カウントが1つ増えます。デクリメントボタンをクリックすると、カウントが1つ減ります。

ステップを変更してインクリメントまたはデクリメントボタンをクリックすると、カウントがステップ数だけ増減します。

useStateフックを使用するカウンターアプリをダウンロードしてください。

以下は、Appコンポーネントのソースコードです。

import { useState } from 'react';
import './App.css';

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  const increment = () => setCount(count + step);
  const decrement = () => setCount(count - step);
  const handleChange = (e) => setStep(parseInt(e.target.value));

  return (
    <>
      <header>
        <h1>Counter</h1>
      </header>
      <main>
        <section className="counter">
          <p className="leading">{count}</p>
          <div className="actions">
            <button
              type="button"
              className="btn btn-circle"
              onClick={decrement}
            >
              -
            </button>
            <button
              type="button"
              className="btn btn-circle"
              onClick={increment}
            >
              +
            </button>
          </div>
        </section>

        <section className="counter-step">
          <label htmlFor="step">Step</label>
          <input
            id="step"
            type="range"
            min="1"
            max="10"
            value={step}
            onChange={handleChange}
          />
          <label>{step}</label>
        </section>
      </main>
    </>
  );
};

export default App;Code language: JavaScript (javascript)

Appコンポーネントには、2つの関連する状態があります。

  • カウント
  • ステップ

また、次の状態は前の状態に依存します。

const increment = () => setCount(count + step);
const decrement = () => setCount(count - step);Code language: JavaScript (javascript)

useStateフックをuseReducerフックに置き換えます。

ステップ1. stateactionの2つのパラメータを持つreducer()関数を定義します

const reducer = (state, action) => {
  // ...
};Code language: JavaScript (javascript)

ステップ2. useStateフックを削除し、代わりにuseReducerフックを使用します

// const [count, setCount] = useState(0);
// const [step, setStep] = useState(1);

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });Code language: JavaScript (javascript)

左側

  • stateは、countstepを含むすべての状態変数を含むオブジェクトです。
  • dispatchは、setCount関数とsetStep関数と同等の機能です。

右側

  • reducerは、stateオブジェクト内のcountプロパティとstepプロパティをどのように更新するかを決定する関数です。
  • {count: 0, step: 1}は初期状態です。

以下の図は、useStateフックとuseReducerフックの同等性を示しています。

React useReducer

状態を更新したい場合は、useReducer()関数の呼び出しから返されるdispatch()関数を呼び出すことができます。

dispatch()関数を呼び出すと、Reactはreducer関数を見つけて実行します。

reducer関数は新しい状態を返す必要があります。何も返さない場合、状態はundefinedになります。

reducer関数は、状態を直接変更してはなりません。代わりに、更新されたプロパティを持つ状態の新しいコピーを返す必要があります。

さらに、reducer()関数には、async/awaitAPIへのリクエストPromiseを含めたり、外部変数を変更したりすることはできません。言い換えれば、純粋関数でなければなりません。

dispatch()関数を呼び出すときは、状態の更新方法をreducer()関数に伝えるためのオブジェクトを渡す必要があります。

慣例により、2つのプロパティを持つactionオブジェクトを渡す必要があります。

{type: 'ACTION', payload: value }Code language: JavaScript (javascript)
  • typeは、どの状態を更新するかをreducerに伝える文字列です。
  • payloadは、reducerに渡される値です。

actionオブジェクトでは、typeプロパティは必須ですが、payloadプロパティはオプションです。

たとえば、ユーザーがインクリメントボタンをクリックした場合、次のようにdispatch()関数を呼び出す必要があります。

dispatch({ type: 'INCREMENT' })Code language: JavaScript (javascript)

reducer()関数では、actionオブジェクトをチェックし、それに応じて状態を更新できます。

if(action.type === 'INCREMENT') {
    return {
        ...state,
        count: state.count + state.step
    };
}Code language: JavaScript (javascript)

同様に、ユーザーがデクリメントボタンをクリックした場合、異なるアクションタイプでdispatch()関数を呼び出す必要があります。

dispatch({ type: 'DECREMENT' })Code language: JavaScript (javascript)

そして、reducer()関数内のactionオブジェクトをチェックして、状態のcountプロパティを減らします。

if(action.type === 'DECREMENT') {
    return {
        ...state,
        count: state.count - state.step
    };
}Code language: JavaScript (javascript)

ユーザーが状態を変更した場合、次のようにdispatch()関数を呼び出すことができます。

dispatch({ type: 'CHANGE_STEP', payload: newStep });Code language: JavaScript (javascript)

reducer()関数では、それに応じて状態のstepプロパティを更新できます。

if(action.type === 'CHANGE_STEP') {
    return {
        ...state,
        step: action.payload
    };
}Code language: JavaScript (javascript)

reducer()関数は多くのactionタイプを処理するため、switch…caseステートメントを使用できます。

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + state.step,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - state.step,
      };
    case 'CHANGE_STEP':
      return {
        ...state,
        count: action.payload,
      };
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};Code language: JavaScript (javascript)

actionタイプが決定されない場合は、エラーをスローするか、無視することができます。

以下は、useStateフックの代わりにuseReducerフックを使用する完全なAppコンポーネントを示しています。

import { useReducer, useState } from 'react';
import './App.css';

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + state.step,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - state.step,
      };
    case 'CHANGE_STEP':
      return {
        ...state,
        step: action.payload,
      };
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};

const App = () => {
  // const [count, setCount] = useState(0);
  // const [step, setStep] = useState(1);

  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

  // const increment = () => setState(count + step);
  // const decrement = () => setCount(count - step);
  // const handleChange = (e) => setStep(parseInt(e.target.value));

  const increment = () => dispatch({ type: 'INCREMENT' });
  const decrement = () => dispatch({ type: 'DECREMENT' });
  const handleChange = (e) =>
    dispatch({
      type: 'CHANGE_STEP',
      payload: parseInt(e.target.value),
    });

  return (
    <>
      <header>
        <h1>Counter</h1>
      </header>
      <main>
        <section className="counter">
          <p className="leading">{state.count}</p>
          <div className="actions">
            <button
              type="button"
              className="btn btn-circle"
              onClick={decrement}
            >
              -
            </button>
            <button
              type="button"
              className="btn btn-circle"
              onClick={increment}
            >
              +
            </button>
          </div>
        </section>

        <section className="counter-step">
          <label htmlFor="step">Step</label>
          <input
            id="step"
            type="range"
            min="1"
            max="10"
            value={state.step}
            onChange={handleChange}
          />
          <label>{state.step}</label>
        </section>
      </main>
    </>
  );
};

export default App;Code language: JavaScript (javascript)

プロジェクトのソースコードをダウンロード

プロジェクトのソースコードをダウンロード

イミュータブルな状態を操作するためのImmerパッケージの使用

reducer関数では、状態を直接変更することはできませんが、変更された状態のコピーを返します。これは非常に不便です。

状態をより便利に操作するために、Immerパッケージを使用できます。したがって、次のように変更された状態のコピーを返す代わりに

return {
  ...state,
  count: state.count + state.step,
};
Code language: JavaScript (javascript)

次のように状態を直接変更できます。

state.count = state.count + state.step;Code language: JavaScript (javascript)

Immerパッケージを使用するには、次の手順に従います。

ステップ1. Immerパッケージをインストールします

npm install immerCode language: JavaScript (javascript)

ステップ2. produce関数をコンポーネントにインポートします

import { produce } from 'immer';Code language: JavaScript (javascript)

ステップ3. 3番目に、reducer関数をproduce関数でラップします

const [state, dispatch] = useReducer(produce(reducer), { count: 0, step: 1 });Code language: JavaScript (javascript)

ステップ4. 状態を直接変更するreducer()関数を簡略化します

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      state.count = state.count + state.step;
      break;
    case 'DECREMENT':
      state.count = state.count - state.step;
      break;
    case 'CHANGE_STEP':
      state.step = action.payload;
      break;
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};Code language: JavaScript (javascript)

プロジェクトのソースコードをダウンロード

まとめ

  • useReducerフックは、useStateフックの代替です。
  • コンポーネントに複数の密接に関連する状態があり、将来の状態が現在の状態に依存する場合は、useReducerフックを使用します。
  • 状態をより便利に操作するには、Immerパッケージを使用します。
このチュートリアルは役に立ちましたか?