本日 (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)