reduxを動かすのにBoilerplate、Middleware、[mapStateToProps]/[mapDispatchToProps]は必要ない

概要

reduxのソースコードを読んだりしている中で気づいたのですが実はreduxを動かすのにタイトルに書いた要素は全て不要です。

Boilerplate、[mapStateToProps]/[mapDispatchToProps]ゼロで使える!

以下はreducerを定義してStoreを作った後ReactからActionをDispatchするサンプルです。

Edit redux-vanila-example

index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { createStore } from 'redux'
import { Provider } from 'redux-vanilla'

const initialState = {
 upVote: 0,
 downVote: 0
}

const reducer = (state, action) => {
 switch (action.type) {
 case 'UP_VOTE':
 state.upVote++
 return { ...state }
 case 'DOWN_VOTE':
 state.downVote++
 return { ...state }
 default:
 return state
 }
}

const store = createStore(
 reducer,
 initialState,
 window.__REDUX_DEVTOOLS_EXTENSION__ !== undefined
 ? window.__REDUX_DEVTOOLS_EXTENSION__()
 : f => f
)

ReactDOM.render(
 <Provider store={store}>
 <App />
 </Provider>,
 document.getElementById('root')
)
App.js
import React, { Component } from "react"
import { connect } from "redux-vanilla"
import styled from "styled-components"
import { Header } from "./Header"
import { Footer } from "./Footer"

export const Container = styled.div`
  text-align: center;
`;
export const Row = styled.div`
  heigh: 60%;
  width: 30%;
  margin: 0 auto;
  display: flex;
  flexdirection: row;
  justifycontent: center;
`;
export const Button = styled.button`
  flex-grow: 1;
  font-size: 20px;
`;
export const Text = styled.h1`
  flex-grow: 1;
  color: ${props => (props.red && "red") || (props.green && "green") || ""}
`

class App extends Component {
  render() {
    const { store, state, dispatch } = this.props

    return (
      <Container>
        <Header />
        <Row>
          <Text red>{state.upVote}</Text>
          <Text green>{store.getState().downVote}</Text>
        </Row>
        <Row>
          <Button onClick={() => dispatch({ type: "UP_VOTE" })}>
            + UpVote
          </Button>
          <Button onClick={() => store.dispatch({ type: "DOWN_VOTE" })}>
            + DownVote
          </Button>
        </Row>
        <Footer />
      </Container>
    );
  }
}

export default connect(App)

↑のコードはreact/reduxのバインディングに自作のredux-vanillaというreact-reduxの劣化版^^;ライブラリを使っていますが、react-reduxでも connect((store) => store)(Component); とすれば全く同じ事が出来ます!

デモ💻

[mapStateToProps]/[mapDispatchToProps]すら省いてreact-reduxを使えることはあまり知られてませんがそれらはそもそもreact-reduxがreduxのStateやロジックをreactと密結合させない高度な設計をしたい人たちに向けて提供しているアドバンスドな機能です。

ReduxはそもそもActionをDispatchするとStateが更新されるだけなんですよね。

reducerやactionを各種ファイルに分割するボイラープレート(actionCreatorとか)は大規模化するアプリに耐えられるように、ReactとReduxを密結合にしたくない、などreduxとは関係ないアプリ設計上のベストプラクティスなので、reduxと一緒に最初からそれらの概念を導入する必要はありません。

非同期処理にMiddlewareは必要ない!

reduxで非同期処理を扱う際にはredux-thunkなどMiddlewareがおなじみですが、実は無くても問題ありません。

以下はMiddleware無しでasync/awaitで非同期処理を扱うコードサンプルです。

コードだけだと何をしているのかピンと来にくいと思うのでデモを先にご覧ください!(Github Pageのリンク先がコードサンプル部分です!)

※flowを使っています。

type Props = { app: ReduxState, dispatch: Dispatch<ReduxAction> }

class Github extends Component<Props> {
  fetchRepository = async () => {
    const dispatch = this.props.dispatch

    // Loading...
    dispatch({ type: type.START_ASYNC })

    // Call API
    try {
      const query = 'react'
      const response = await axios.get(
        `https://api.github.com/search/repositories?q=${query}`
      )
      const repositoryList: RepositoryList = response.data.items

      dispatch({
        type: type.ASYNC_FETCH_REPOSITORY,
        payload: { repositoryList: repositoryList }
      })
    } catch (e) {
      console.error(e)
    }
  }

  componentDidMount() {
    // ページロード時に呼び出されます
    this.fetchRepository()
  }

  render() {
    const { isLoading, repositoryList } = this.props.app
    const repoList = this.getRepoList(repositoryList)

    return (
      <Container>
        <Header>Github Page</Header>
        <List>{isLoading ? <Loading /> : repoList}</List>
      </Container>
    )
  }

  getRepoList(repositoryList: RepositoryList): React.Element<any> {
    return repositoryList.length ? (
      repositoryList.map((r: Repository) => (
        <Item key={r.id}>
          <p>{r.name}</p>
          <p>{r.description}</p>
          <p>{r.full_name}</p>
          <p>{r.owner.login}</p>
          <img src={r.owner.avatar_url} alt="avatar" />
        </Item>
      ))
    ) : (
      <p>no items.</p>
    )
  }
}

export default connect((state: RootReduxState) => state)(Github)

コードベース全体はredux-no-middleware-pattarnに公開しています。

デモを見ると分かると思いますがAPIからの情報取得(ASYNC_FETCH_REPOSITORY)が非同期処理です。完了するまでくるくるとスピナーが表示されています。

コンポーネントクラスに非同期メソッドfetchRepository()を定義してcomponentDidMount()(実質的にページロード時)でそれを呼び出す直接的な実装です。
ボタンをクリックした時にfetchRepository()を起動させたければrender()内で任意の要素のonClickに指定してあげればOKです。

もし複数のコンポーネントからfetchRepository()が呼ばれるようであればactionCreatorパターンを用いてロジックを共通化すると良いでしょう。
その場合でもMiddlewareは必要ありません。

個人的には次のようなモチベーションでこのパターンを好んでいます。

  • JavaScriptがasync/awaitで簡単に非同期処理を扱えるようになった。
  • reduxはDispatchしたActionに応じてStateを更新する作業だけやってくれれば良い。
  • むしろそれ以外を請け負って複雑になって欲しくない
    • (redux-thunkで実装が複雑になるわけでは全くない。がreduxと非同期処理の異なる2つの関心ごとがセットになる事でreduxについて話す時にStateManagementライブラリに非同期処理も内包される、というコミュニケーション上の複雑性が増大するのが個人的に好きではない)ので非同期処理のハンドリングはプログラマ側で行う。

まとめ

そもそもreduxが複雑に感じる要因は多くの記事でReduxをReactから使う方法React & Reduxを用いて高品質かつスケールするアプリを設計するプラクティスが一緒に扱われていることではないでしょうか。

参考:(めっちゃ良記事です!)
それでもやっぱり redux は面倒くさい
Reduxは不要ではないか?

もちろん大規模な開発やテストを書きたい時は巷で紹介されているパターンを利用しますが、私個人最初はこんな感じでカジュアルにreduxを使っています!

ActionをDispatchするとStateが更新されるだけなのにどうしてこんなに解りにくいのか?疑問に思っていた方の一助にならば、
またこれまでreduxを諦めていた方々やこれから始めようと思っている方に楽しくreduxを使って欲しくてこの記事を書きました!

最後まで読んで頂きありがとうございます!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.