31
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?