文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

react-redux v7.1+TypeScriptでconnect, mapStateToProps, mapDispatchToPropsを撲滅する

ついに例の定型文3兄弟を除去する事ができるようになりました〜


f:id:treeapps:20190616112954p:plain

github.com

先日react-reduxがv7.1にアップデートされ、そこでhooks対応の関数がいくつか追加されました。

今回紹介するものは「useSelector」「useDispatch」の2つです。

react-redux v7.1の新機能

useSelector

ざっくり説明すると、mapStateToPropsをhooks対応したものです。

useSelectorを使う事で、mapStateToPropsを撲滅する事ができるようになります。

useDispatch

ざっくり説明すると、mapDispatchToPropsをhooks対応したものです。

useDispatchを使う事で、mapDispatchToPropsとbindActionCreatorsを撲滅する事ができるようになります。

connect関数は不要になる

useSelectorもuseDispatchもhooks apiで実装されているため、HOCベースなconnect関数は不要になります。これは非常に大きい事で、関数のexport周りのコードが凄くすっきりできます。

v7.1とそれ以前のコードの比較

v7.1以前のTypeScript + react-reduxのコード

まずは今までの見慣れたTypeScript + reduxの伝統的なコードです。これでもextends Rect.Componentなコンポーネントでないのでまだマシです。

mapStateToPropsより下辺りのコードがもう定型文と化していますね。

import { Button } from "@material-ui/core"
import { createStyles, withStyles, WithStyles, Theme } from "@material-ui/core/styles"
import React from "react"
import { connect } from "react-redux"
import { CounterActions } from "../store/actions"

const styles = (theme: Theme) => createStyles({
  root: {},
})

interface IProps extends WithStyles<typeof styles> {
  count: number
  increment: () => number
  decrement: () => number
}  

export const Redux = (props: IProps) => {
  const { classes, count } = props
  const handleIncrement = () => props.increment()
  const handleDecrement = () => props.decrement()
  return (
    <div className={classes.root}>
      <p>{count}</p>
      <Button color="primary" onClick={handleIncrement}>+ 1</Button>
      <Button color="primary" onClick={handleDecrement}>- 1</Button>
    </div>
  )
}

const mapStateToProps = (state: IInitialState) => ({
  count: state.counter.count,
})

const mapDispatchToProps = (dispatch: Dispatch<Action<any>>) =>
  bindActionCreators(CounterActions, dispatch)

export default withStyles(styles)(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Redux as any)
)

※ 汚さを際立たせるため、敢えてwithStylesを使っています。

このコードを見ると私は以下を考えてしまいます。

  • propsにstateやactionをコピーするせいで、IPropsに本来のプロパティ値以外のreduxでしか使わないものが混入している。
  • withStylesとconnectのHOCを多重ラップしている部分の冗長さ。
  • HOCのラップする順番とかどうなんだっけ?等と考える面倒さ。
  • mapDispatchToPropsの型定義部分の嫌さ。

v7.1以降のTypeScript + react-reduxのコード

import { Button } from "@material-ui/core"
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"
import React from "react"
import { useDispatch, useSelector } from "react-redux"
import { CounterActions } from "../store/actions"
import { IInitialState } from "../store/states"

const useStyles = makeStyles((theme: Theme) => createStyles({
    root: {},
}))

const countSelector = (state: IInitialState) => state.counter.count

export default function() {
  const classes = useStyles({})
  const dispatch = useDispatch()
  const count = useSelector(countSelector)
  const handleIncrement = () => dispatch(CounterActions.increment())
  const handleDecrement = () => dispatch(CounterActions.decrement())

  return (
    <div className={classes.root}>
      <p>{count}</p>
      <Button color="primary" onClick={handleIncrement}>+ 1</Button>
      <Button color="primary" onClick={handleDecrement}>- 1</Button>
    </div>
  )
}

滅茶苦茶コードがスッキリしましたね!

今までconnect + mapStateToProps + mapDispatchToPropsの定型文3兄弟は、確かにプレーンで処理が(ある程度)解りやすい面がありましたが、この定型文を覚えたりコピペするのが結構面倒なのは皆思っていた筈なので、これが無くせるのは大きいですね。

mapStateToPropsはselectorになっただけなのでほとんど変わってないように見えますが、connect関数の引数としての関数だったのが、connectが消えた事で純粋なstateのセレクタになり、より役割が解りやすくなったのではないかと思います。angularのngrxで言うところのcreateSelector()に近い形になりました。

ngrxのcreateSelector()風に使うなら、セレクタ関数はstoreパッケージに移動すれば、更にコードがスッキリし、役割も更に明確化しそうですね!

全部入りのサンプルコード

github.com

TypeScript v3.5 + Next.js v8.1 + material-ui v4 + react-redux v7.1に対応したサンプルプロジェクトになります。

Next.jsなので、勿論SSR対応しており、このサンプルではSSRはpagesディレクトリ配下で使用しています。

コンポーネント自体は単純なfunctionで記述しておき、functionにgetInitialPropsをstaticメソッドとして定義し、最後にexport defaultする形になります。

function Redux() {
  // snip
}
// for SSR
Redux.getInitialProps = async ctx => {
  const pagePayload: IPagePayload = {
    selectedPage: Page.REDUX,
  }
  ctx.store.dispatch({
    type: PageActions.changePage.toString(),
    payload: pagePayload,
  })
}

export default Redux

万能ではない点に注意

Note: The selector function should be pure since it is potentially executed multiple times and at arbitrary points in time.
注:セレクター関数は、複数回、任意の時点で実行される可能性があるため、純粋でなければなりません。

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

また、私もまだ触ったばかりで把握しきれてないですが、以下の問題もあるため、よくドキュメントを読んでおいた方が良さそうです。
react-redux.js.org

雑感

hooksにより、相当シンプルにコードが記述できるようになりました。

もはや定型文と化していたconnect + mapStateToProps + mapDispatchToPropsの3兄弟がいなくなったのは大きいですね。

今回の機能はまだリリースされたばかり解っていない部分があったりドキュメントが読み込めてないので、随時キャッチアップして以下のリポジトリに反映していきたいと思います!
github.com