All Articles

react-redux の Hooks API を試してみる

はじめに

現状、React Redux (react-redux) では mapStateToPropsmapDispatchToProps を定義し、 Component/Container を connect() でラップし、 HOC とする事で Redux の Store を連携させています。 この記述は毎度同じような少し長いコードを書かないといけなく、若干つらみがある部分でした。

さて、React v16.8 から Hooks API が提供されたことを受け、 React Redux でも先日 Hooks API が提供されました。
v7.1.0 から利用可能なので、早速試してみます。

https://react-redux.js.org/next/api/hooks

試してみる

毎度お馴染みのカウンターを実装します。

import { combineReducers } from 'redux'
import { ActionType, action } from 'typesafe-actions'

// constants
const COUNTER_INCREMENT = 'counters/INCREMENT'
const COUNTER_DECREMENT = 'counters/DECREMENT'
const COUNTER_RESET = 'counters/RESET'

// actions
const incrementCounter = () => action(COUNTER_INCREMENT)
const decrementCounter = () => action(COUNTER_DECREMENT)
const resetCounter = () => action(COUNTER_RESET)
const actions = {
  incrementCounter,
  decrementCounter,
  resetCounter
}
type CounterAction = ActionType<typeof actions>

// reducers
type CounterState = {
  readonly count: number
}
const countersReducers = combineReducers<CounterState, CounterAction>({
  count: (state = 0, act) => {
    switch (act.type) {
      case COUNTER_INCREMENT:
        return state + 1
      case COUNTER_DECREMENT:
        return state - 1
      case COUNTER_RESET:
        return 0
      default:
        return state
    }
  }
})

export {
  CounterState,
  CounterAction,
  countersReducers,
  incrementCounter,
  decrementCounter,
  resetCounter
}

これを SFC から呼び出してあげます。

useSelector

mapStateToProps の代わりとして使う useSelector が提供されています。
公式のサンプル(JS)では以下のように SFC で Hooks API を呼び出し、Store から欲しい値を取り出せている事がわかります。

import React from 'react'
import { useSelector } from 'react-redux'

export const CounterComponent = () => {
  const counter = useSelector(state => state.counter)
  return <div>{counter}</div>
}

なお、useSelector の定義は以下のようになっており、第二引数では再レンダリングのための比較の関数を渡す事ができます。
デフォルトは Strictly Equal (===) での比較です。この比較で異なると、再レンダリングがかかるようになっています。

useSelector(selector : Function, equalityFn? : Function)

なお、複数のフィールドの値を1つの useSelector で取得する場合は、全体のオブジェクト変更毎に毎度再レンダリングがかかるようになるので、reselect を使うか、第二引数に react-reduxshallowEqual を渡すようにする必要があります。
なお、 connect() では単一のオブジェクトを返す仕組みのため、 shallowEqual がデフォルトの挙動でした。この点は注意が必要です。

useDispatch

mapDispatchToProps の代わりとして使う useDispatch が提供されています。
これは dispatch を返す Function です。これが今まで欲しかった・・・・。

公式のサンプルでは以下のように使用されています。

import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
  const dispatch = useDispatch()

  return (
    <div>
      <span>{value}</span>
      <button onClick={() => dispatch({ type: 'increment-counter' })}>
        Increment counter
      </button>
    </div>
  )
}

ハンドラにそのまま dispatch を渡せるので、シンプルに実装が可能となっています。
素晴らしい。。

実装

useSelector 及び useDispatch を用いてカウンターを実装してみました。
これで殆どの Component/Container を SFC で組めるようになるのではないでしょうか。

import * as React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { Statistic, Button } from 'semantic-ui-react'

import { ReducerState } from '@/store/root-reducer'
import {
  incrementCounter,
  decrementCounter,
  resetCounter
} from '@/features/counter/widget'
import * as style from './Counter.scss'

const CounterComponent = () => {
  const dispatch = useDispatch()
  const count = useSelector((state: ReducerState) => state.counter.count)
  return (
    <div>
      <div className={style.counter}>
        <Statistic>
          <Statistic.Value>{count}</Statistic.Value>
          <Statistic.Label>Counts</Statistic.Label>
        </Statistic>
      </div>
      <Button.Group>
        <Button onClick={() => dispatch(incrementCounter())}>+1</Button>
        <Button onClick={() => dispatch(decrementCounter())}>-1</Button>
        <Button onClick={() => dispatch(resetCounter())}>Reset 0</Button>
      </Button.Group>
    </div>
  )
}

export default withRouter(React.memo(CounterComponent))

https://github.com/pm11/react-redux-hooks-example に検証コードを置いています。

まとめ

従来の React Redux の connect を用いたコードをかなりコンパクトに書けるようになっています。
注意する点はありそうですが、積極的に使っていきたいと思います。

Ref