【React学習】〜useReducer〜

React

 Reactを自由自在に扱えるわけではないので大きなことは言えませんが、Reactを理解する(あるいはReactを使う)ことは、React Hooksの理解(使用)と関係が深い気がします。公式ドキュメントにそれぞれのHooksの説明がありますから、用途や使い方を学ぶことができます。ただ、実際に自分で作ってみると(ドキュメント通りに作ればしっかり動作しますが、自分で考えて作ってみるのも大切だと思います)、いろいろと壁に当たってしまいます。

useReducer

 アプリを作ってて、何のためにあるのかよくわからなかったのがuseReducerというHookです。Udemyの講座でも登場し、意味は分かったんですが、用途がよくわからなかったんです。講座では、useStateとほとんど同じ役割で、更新用関数を「誰が」定義するのかの違いだと言っていました。

  • useState→stateの更新は「コンポーネント」が行う。
  • useReducer→stateの更新は「stateを用意した人」が行う。

 こんな感じだったと思います。

const [ val, setVal ] = useState(0);

const handler = () => {
  setVal( prev => prev + 1 );
}
const [ val, dispatch ] = useReducer( prev => prev + 1, 0 );

const handler = () => {
  dispatch();
}

 上記の2つの例は同じ動作をします。2つの決定的な違いは、「state(今回はval)を宣言したときに、更新方法を指定している」かどうかです。

 useStateの方は初期値を与えただけで、更新方法はhandler関数に書かれています。一方useReducerの方は第1引数に渡したコールバック関数(リデューサー関数)で更新方法を指定しています。handler関数で行っているのは、その更新関数の「実行」のみとなっています。

関心の分離

 Reactの魅力の一つはコンポーネントの再利用だと思いますが、アプリの規模が大きくなってくると、当然再利用の回数が増えてきます。そうすると「同じ値を扱っているのに、コンポーネント(例えばボタンなど)によって処理が異なる」という場面に出くわします。

 useStateを使うと、コンポーネントの中に(コンポーネントを表す関数の中に)処理を記述することになります。すると、だんだんコード量が増えていくんですね。この処理のときはこの関数で、この処理のときは・・・というように、コンポーネントに「ロジック」が積み重なっていくんです。

 コンポーネントはあくまで「部品」であって、細かい処理まで分担させるべきではありません。こうした考えを「関心の分離」というそうです。それぞれがそれぞれの役割に集中せよ、と解釈しています。

 つまり、ユーザーが目にする「部品」としての働きと、裏で行っている「処理」の部分を、同じコンポーネントファイルに記述しないようにする、ということです。ここで活躍するのがuseReducerです。

reducer関数とは

 useReducerに渡すコールバック関数はreducerと呼ばれます。reducerは純粋であることが求められます。先程の例では元の値を+1する処理しか渡していませんが、dipatchに渡すactionという引数によって、処理を分岐することができます。

const [ val, dispatch ] = useReducer( reducer, 0 );

const reducer = (action) => {
  switch( action.type ){
    case "increment":
      return action.payload + 1;
    case "decrement":
      return action.payload - 1;
    default:
      return action.payload;
  }
};

const handler = () => {
  dispatch( { type:"increment", payload: 1 } );
}

 useReducerに渡すコールバックをreducerとして別に定義し、その中でaction.typeによって処理を分岐しています。一般に引数として渡すactionはオブジェクトで、typeプロパティは処理の種類を、payloadプロパティは値を表しています。上の例では、handlerが実行されると、payloadの値+1がvalに格納されて再レンダリングされます。

 reducer関数は純粋なので、この関数自体はどこで実行しても外部に影響はありません。さらに、useReducerはコンポーネント内部で宣言しますが、reducer自体は外部ファイルに記述しても大丈夫です。つまり、stateを更新する処理をコンポーネント外部に持っていくことができるわけです。

 もっと良いことは、handlerの中に書く処理が「何をする」という意味になることです。上の例ではpayloadの値をincrementすることがひと目で分かります。今回は+1するだけの簡単な処理ですが、複雑な処理だとしても、その名前(種類)と値さえ渡せば実行できるので、handlerの中に処理を記述する必要がなくなるのです。

 このようにコンポーネントの中で行われる処理を外部に記述して、コンポーネントの役割と処理を分離することができるのが、useReducerの魅力だと思います。

useContextとの組み合わせ

 useContextと組み合わせると、アプリ全体で使う値と処理を簡単に管理することができるようになります。

export const ValueContext = createContext(null);

const MyProvider = ({ children }) => {
  const [ value, dispatch ] = useReducer( reducer, defaultValue );

  return (
    <ValueContext.Provider value={ { state, dispatch } }>
      { children }
    </ValueContext.Provider>
  );
}

//valueとdispatchを使うコンポーネント
const Example = () => {
  const { value, dispatch } = useContext(ValueContext);
  ...
}

 このように、useReducerで定義したvalueとdispatchをcontextのvalueとしてProviderを作成し、アプリ全体をラップすることで、valueとdispatchを使いたいコンポーネントの内部でcontextを読み取って実行できるようになります。記述していませんが、reducerは別ファイルで管理できますので、アプリ全体で処理の変更が必要になっても、reducerを書き換えるだけでOKです。

コメント