はじめて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つ目の引数の配列の値が同じである限りキャッシュする」という機能ですが、これを見ればdispatch
はsetState
にしか依存していないことがわかるかと思います。
そして、この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});