Redux 入門(サンプルコードを読む)

React/Redux初心者の私が、Reduxのサンプルコードを見てみる回です。 https://github.com/reduxjs/redux/tree/master/examples

対象としては「用語はわかるけどどうやって使うのかいまいちわからん」な人です。 つまり私です。

ディレクトリ構成

スクリーンショット 2020-06-02 2.46.05.png

見てみる。

手元でビルドできない。

import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)

export default App

まあ、Todo作成する部分と、表示する部分と、フッターがあるみたい。 表示する部分から見てみよう

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { VisibilityFilters } from '../actions'

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

おっ、Viewを持っていない? Viewの本体はこっちみたいだ。

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => toggleTodo(todo.id)}
      />
    )}
  </ul>
)

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
  }).isRequired).isRequired,
  toggleTodo: PropTypes.func.isRequired
}

export default TodoList

connectってなんだろう?

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

なんだろう。 Reduxライブラリからimportしている。

https://react-redux.js.org/api/connect

The connect() function connects a React component to a Redux store.

おー。つまりVisibleTodoList.jsがStoreなんだね。 StoreがViewに依存する状態。

じゃあこの引数で渡しているのは何?

mapStateToProps, mapDispatchToPropsってなんだろう?

React ComponentとStoreをconnectする(きっと、StoreがComponentに対してイベントを送れる状態)時に引数で渡すこのふたつ。

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

stateはまあわかる。

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}

リストとTypeを渡すとフィルターしたTodoのリストを返すような関数を渡している。 それがPropsとしてViewに渡り、それをmapして描画している。 じゃあgetVisibleTodoListに引数を渡すのは誰なんだろう?

とりあえずその下、dispatchも見てみよう。

const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})

toggleTodoというのはActionだ。

export const toggleTodo = id => ({
  type: 'TOGGLE_TODO',
  id
})

StoreにtoffleTodoというActionをディスパッチしている。で、それもViewにconnectされる。 Viewの中で

const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => toggleTodo(todo.id)}
      />
    )}
  </ul>
)

というのがあるから、セルをタップするとそのTodoの状態が変わるのかな。 このActionがどうやってViewに伝わるのか。Reducerにわたるはずやけど。

と思っていたら、鍵はこれだった

import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'

const store = createStore(rootReducer)

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

いっちばん上でrootReducerからStoreを作って、下位コンポーネント全てにぶん投げている。 つまり単一のストアをアプリケーション全体で共有している状態。 そしてこのrootReducerというのは

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'

export default combineReducers({
  todos,
  visibilityFilter
})

こいつを指している。 これはさらに

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        (todo.id === action.id)
          ? {...todo, completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}

export default todos

こいつらを表している。

rootReducerってなんだ?

React.js - rootReducerってなんですか?|teratail なるほど。。

createStoreとは?

reducerからStoreを作る。 Storeはreduser毎にstateを作成する?

Storeとは?

わからんち

Providerとは?

  1. Reactコンポーネント内でreact-reduxのconnect()関数を使えるようにすること
  2. ラップしたコンポーネントにstore情報を渡すこと

qiita.com

これってもしかして、その下位コンポーネントがStoreの役割を持てる(connectとかdispatcherを自分の関数のように扱える)ことと関係がある?ありそう。 Providerしたらその下位コンポーネントは渡したStoreの役割を持てるっぽい。

一旦まとめ

流れとしては、 1. ルートのindex.jsが呼ばれる 2. rootReducer(reducersディレクトリ 直下のcombineReducer)を使ってStoreを作成する 3. storeが下位コンポーネントにぶん投げられる?(Provider) 4. VisibleTodoList.jsでViewとStoreをconnectする(その時にストアが更新されるたび呼び出される関数とstateを書き換えるdispatchを定義する) 5. Viewでdispatchを呼び出す 6. dispatchがアクションを発火させてreducerがstoreのstateを更新する 7. storeの更新を検知してViewのストアが更新されるたび呼び出される関数が走る 8. それに応じてViewが再レンダリングされる

参考リンク

github.com

redux.js.org

note.com

qiita.com