Help us understand the problem. What is going on with this article?

React Hooksでたどる、stateからreducerまで

More than 1 year has passed since last update.

はじめてReduxの流れを見たときに、複雑さで戸惑いましたが、Reactの状態遷移でいろいろやっていった結果、より基礎的なところから組み立てたほうが、Reducerという世界観を理解できる気がしてきました。

なお、後述の事情により、React Hooksで進めています。また、説明のために必要な箇所を除いて、useCallbackは省略します。

useStateの基本

シンプルな値の格納

React.useStateを使えば、stateとそれを設定する関数を得ることができます。

const [state, setState] = React.useState(0);

<button type="button" onClick={() => setState(1)}>1をセット</button>

「値の設定」と「取得」という、状態を記憶させる上で基本となる機能です。

前の値を使った更新

将来のReactでは非同期レンダリングが入るとのことで、外側にあるstateを読んでsetStateをかけるという方法は推奨されていません。代わりに、setState前の値 => 次の値という形の関数を渡すことで、前の値を使っての更新が可能です。

const [state, setState] = React.useState(0);

<button type="button" onClick={() => setState(x => x + 1)}>インクリメント</button>

オブジェクトをstateにする

useStateの基本機能は上述のとおりですが、stateとしていくつもキーがあるオブジェクトを入れて、更新に際しては一部だけ動かす、というようにする場合、関数による更新が必須となります(そうしないと残りのキーが吹き飛んでしまいます)。

const [state, setState] = React.useState({text: "hoge", count: 1});

const onChangeText = e => {
  const text = e.target.value;
  setState(oldState => ({...oldState, text}));
}

<input type="text" value={state.text} onChange={ onChangeText } />

dispatch関数の抽出

上のonChangeTextでは「更新する値の作成」と「更新の反映」が一体となっていますが、これを切り離すことも可能です。

const [state, setState] = React.useState({text: "hoge", count: 1});

const dispatch = React.useCallback(
  updates => setState(oldState => ({...oldState, ...updates})),
  [setState]
);

const onChangeText = e => {
  const text = e.target.value;
  dispatch({text});
}

<input type="text" value={state.text} onChange={ onChangeText } />

React.useCallbackは、「引数として与えられた関数オブジェクトを、2つ目の引数の配列の値が同じである限りキャッシュする」という機能ですが、これを見ればdispatchsetStateにしか依存していないことがわかるかと思います。

そして、このdispatchは、そのまま「stateの中で更新したいデータを入れれば、その更新を行う」という関数として、stateを引き回さなくても使えるようになります。Contextに入れて下位コンポーネントまで流すのも便利です。

じつは、クラスコンポーネントのthis.setStateは、標準で「オブジェクトのマージ」を行う、これぐらい複雑な機能(関数更新もあるのでもっと複雑か)を持ったメソッドなのでした。よりシンプルを期すために、useStateから始めていました。

reducerの抽出

さらに、「従来のオブジェクトと更新内容から新しいオブジェクトを作る」という部分も、これ単体で抽出が可能です。

// コンポーネントの外に置いても問題なし
const reducer = (oldState, action) => ({...oldState, ...action});

const [state, setState] = React.useState({text: "hoge", count: 1});

const dispatch = React.useCallback(
  updates => setState(oldState => reducer(oldState, updates)),
  [setState]
);

const onChangeText = e => {
  const text = e.target.value;
  dispatch({text});
}

<input type="text" value={state.text} onChange={ onChangeText } />

これだけ見れば、reducer関数を切り出すのは過剰にも思えます。ただ、このように分離することで、reducer部分は「引数のみに依存する、純粋な関数」として構築できるようになり、コンポーネントの状態管理など無関係に、データの更新へ専念できるようになります。

そして、actionも単にマージするだけではなく、例えば動作内容をaction.typeで切り分けるというような、Reducer内で複雑な値の操作を行う方向へ発展させることもできます。

一方で、reducerからdispatchを作るのは定型化していますので、React.useReducerというHooksが用意してあります。

const [state, dispatch] = React.useReducer(reducer, {text: "hoge", count: 1});
jkr_2255
qiitadon
Qiitadon(β)から生まれた Qiita ユーザー・コミュニティです。
https://qiitadon.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away