Posted at

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

はじめて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});