JavaScript
React
redux
react-hooks

Redux Hooks によるラクラク dispatch & states

本日 (2019/06/11) React-Redux の version 7.1 がリリースされて、 Redux Hooks が使用できるようになりました。

Redux Hooks によって、connect() を利用しなくても、各コンポーネントとdispatch, state が簡便に利用できるようになります。

サンプルコードです。Action, Reducer 部分は省略です。

ただシンプルにカウンターの数値を増減させるだけのコンポーネントです。

import React from "react";

import { useDispatch, useSelector } from "react-redux";
import { incrementAction, decrementAction } from "./Actions";

const counterSelector = state => state.counter;

const Counter = props => {
// useDispatch で store に紐付いた dispatch が取得できます。
const dispatch = useDispatch();

// useSelector で store の state が取得できます。
const counter = useSelector(counterSelector);

return (
<>
<p> count: {counter} </p>
<button onClick={dispatch(incrementAction())}>increment</button>
<button onClick={dispatch(decrementAction())}>decrement</button>
</>
);
};

useSelector と useDispatch を使わない場合だと、以下のようになります。

import React from "react";

import { connect } from 'react-redux'
import { incrementAction, decrementAction } from "./Actions";

const counterSelector = state => state.counter;

const CounterComponents = props => {
return (
<>
<p> count: {props.counter} </p>
<button onClick={props.increment}>increment</button>
<button onClick={props.decrement}>decrement</button>
</>
);
};

const mapStateToProps = state => ({
counter: counterSelector(state)
});

const mapDispatchToProps = dispatch => ({
increment: () => dispatch(incrementAction()),
decrement: () => dispatch(decrementAction())
});

const Counter = connect(
mapStateToProps,
mapDispatchToProps
)(CounterComponents);

Redux Hooks を利用すると connect 部分の記述がなくなり、かなりスッキリしたコードになるのがわかると思います。


useSelector()

store から state を取得する場合、useSelector() を呼びます。useSelector の引数には state を引数にとる selector 関数を指定します。

const counterSelector = (state) => state.counter

const Counter = (props) => {
const counter = useSelector(counterSelector)
return <div>{counter}</div>
}


useDispatch()

useDispatch で store に紐付いた dispatch が取得できます。

const Counter = (props) => {

const dispatch = useDispatch()

return (
<>
<button onClick={()=> {dispatch({ type: "INCREMENT_COUNTER" })}}>increment</button>
<button onClick={()=> {dispatch({ type: "DECREMENT_COUNTER" })}}>decrement</button>
</>
)
}

子コンポーネントに dispatch を渡す場合、useCallbackを利用して、メモ化することをオススメします。親の再レンダリングによって子コンポーネントが不必要に再レンダリングされることを回避するためです。

import React, { useCallback } from 'react'

import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)

return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}

export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))


注意点

mapStateToProps を利用する場合と異なり、useSelectorには若干の問題が含まれています。

とくに useSelector()props に依存するとき問題が発生しえます。参考:Usage Warnings

dispatchが実行されるたびに、useSelector() は実行されます。このとき、props が最新の状態である保証がありません。


  • dispatch() 実行され、コンポーネントの useSelector() が実行される。

  • 親のコンポーネントが再レンダリングされ、props がコンポーネントに渡される。

  • props が渡される前に、useSelector() が実行されているため、selector 関数がprops に依存していた場合、不整合が生じる。

対策としては以下のとおりです。


  • selector 関数では props を利用しない。

  • selector 関数が props に依存していて、状態変化に基づいて props が変化したり、state のデータが削除される場合、慎重に利用してください。state.todos[props.id].name などというように利用するのではなく、まずstate.todos[props.id] でデータの存在を確認してから、そのあとにtodo.name を呼び出してください。


所管

useContext と useReducer の組み合わせによって、簡易的なグローバル state を用意する方法は React Hooks が発表されてから提案されていました。

それと同じくらい簡便に Redux が利用できるのはかなり魅力的だと思います。

v7.1.0-alpha.5 のころから、ちょこちょこ利用してみましたが、なかなか便利です。

……やや取扱に注意が必要ですけど。


まとめ

以上、React Redux 7.1 から正式リリースとなった Redux Hooks についてでした。

こうした方がいいという知見がありましたら、コメントのほうに書いていただければ嬉しいです。

react-starter-kit を用いれば、更にコードの記述量が減らせて開発が楽になります。こちらも紹介記事を書きました。

Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。そしてデバッグはやりやすく ( redux-starter-kit)