JavaScript
reactjs
redux

Redux ExampleのTodo Listをはじめからていねいに(1)

More than 1 year has passed since last update.

Reduxの公式ExampleにあるTodo Listを少しづつ書いてみました。

最終的な動作イメージは以下のような感じです。
Github Pagesにもあげていますので、触ってみてください。

redux-todos.gif

基本的なReduxの説明や環境構築などは他のドキュメントに譲るとして、
ここでは各機能ごとに動くものを作りながら、最終的にExampleの形になることを目指します。
Reactがだいたい分かってて(チュートリアルをやったくらい)で、
Reduxを触り始めたくらいの人を対象としています。

Reduxを触り始めて日が浅いので、間違いもあるかと思います。ぜひご指摘ください。

ExampleのTodo Listの機能は次の3つです。

  1. TodoをTodo Listに追加する「Add Todo」
  2. Todoの完了・未完了を切り替える「Toggle Todo」
  3. 表示するTodo Listを完了または未完了のTodoだけにする「Filter Todo」

また、動かした環境は次の通りです。

  • react: 0.14.7
  • redux: 3.2.1
  • babel: 6.5.2

今回は、「Add Todo」の機能を実装します。
最初なのでボリュームが多くなったので、「Add Todo」実装は2回に分けます。
1回にまとめました。

1. Hello World

何はともあれHello Worldを表示させます。

react-reduxのProviderがreactのコンポーネントをラップしており、
これが最上位になります。

index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'

render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById('root')
)

Appコンポーネントでは、Hello Worldを表示させているだけです。

components/App.js
import React from 'react'

const App = () => (
  <div>
    Hello World!
  </div>
)

export default App

ここまででとりあえず、動きます。
Providerはstoreを呼ぶ必要があるので、Warningが出ます。

Warning: Failed propType: Required prop `store` was not specified in `Provider`.
Warning: Failed Context Types: Required child context `store` was not specified in `Provider`.

2. actionCreatorで発行したactionをreducerに渡してstoreのstateを更新する

Actions

addTodoというactionCreatorを作ります。
actionCreatorは純粋な関数であり、actionを発行するだけです。

actionはactionTypeとデータで構成されるオブジェクトです。
ここでは、ADD_TODOがactionTypeで、データはidとtextです。
(todoは複数なので、idは後々、必要になります。)
データは、配列でもオブジェクトでも、関数でもなんでもいいです。

actionCreatorによって作られたactionはdispatchに渡されます。

actions/index.js
let nextTodoId = 0
export const addTodo = (text) => {
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  }
}

Reducers

reducerも単なる関数で、現在のstateとactionを受け取り、新しいstateを返します。
todoというreducerは、actionTypeがADD_TODOのとき、{ id: action.id, text: action.text }という新しいstateを返します。

reducers/index.js
const todo = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text
      }
    default:
      return state
  }
}
export default todo

Store

storeはアプリケーションで単一のもので、stateを保持します。
createStore関数でreducerを呼び出すことで作られます。
また、Providerにはstoreを渡します。これで、Warningは消えるはずです。

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

let store = createStore(todo)

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

ここまでで、action -> reducer -> storeの一連の流れができました。
実際にstateが更新されるのを見てみます。

  1. actionCreatorであるaddTodo('Hello World!')で{ id: 0, text: 'Hello World' }というactionが作成される。
  2. store.dispatch関数にactionを渡すことで、todo reducerにaction = { id: 0, text: 'Hello World' }, state = 現在のstate(初期値を与えてないのでundefinedだと思います。)
  3. todo reducerでは、新しいstate = { id: 0, text: 'Hello World' }を返します。

storeが保持しているstateはstore.getStateで取得できます。

index.js
import { createStore } from 'redux';
import todo from './reducers';
import { addTodo } from './actions'

let store = createStore(todo)

store.dispatch(addTodo('Hello World!'))
console.log(store.getState()) // => Object {id: 0, text: "Hello World!"}

3. storeで保持したstateをViewで表示する

Todo Listの作成

viewで表示する前に、少しだけreducerを書き換えます。
stateでtodoを保持することはできましたが、このままでは1つのtodoしか
保持できないので、複数のtodoを保持できるよう拡張します。

storeの生成もcreateStore(todos)とすることで、todos関数が呼び出され、
その中でtodo関数が呼び出されます。

reducers/todos.js
const todo = (state, action) => {
  ...
}

const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        todo(undefined, action)
      ]
    default:
      return state
  }
}

export default todos

また、今後のことを考えtodosのreducersを別ファイル(index.jsをtodos.js)にしています。
exampleではtodos以外のreducerを使うからであって、1つしか使わない場合は必要ないです。

combineReducersでは、その名の通り複数のreducersを結合することができます。
createStore(todos)では、state = [todo1, todo2]となりますが、
結合されたtodoAppをつかったcreateStore(todoApp)では、
state = {todos = [todo1, todo2]}といった形で保持されます。

reducers/index.js
import { combineReducers } from 'redux'
import todos from './todos'

const todoApp = combineReducers({ todos })
export default todoApp

ContainerとComponent

viewはreactのコンポーネントで実装されますが、大きく2種類あります。
公式サイトによると、

  • Presentational Components
    • Reactでいう普通のコンポーネント。
    • Reduxの要素はないので、react-reduxはimportしない。
    • データは上位のコンポーネントからpropsとして受け取る。
    • データの変更もpropsからコールバックさせて行う。
  • Container Components
    • Redux特有のもので、react-reduxをimportする。
    • データは、storeのstateから渡される。
    • データの変更もactionをdispatchに渡すことで行われる。
    • connect()を使ってコンポーネントをラップすることで、dispatchやstateが使えるようになる。

ToDoコンポーネントとToDoListコンポーネントを作る

Todoコンポーネントは、propとして渡されてきたtextを表示するだけです。

components/Todo.js
import React, { PropTypes } from 'react'

const Todo = ({ text }) => (
  <li>
    {text}
  </li>
)

Todo.propTypes = {
  text: PropTypes.string.isRequired
}

export default Todo

TodoListコンポーネントは、propとして渡されてきたtodosの各要素をTodoコンポーネントに
渡します。ここで、配列としてコンポーネントを複数生成するときkeyが必要になります。
{...todo}はtodoのすべての要素です。id={todo.id} text={todo.text}と同じです。

components/TodoList.js
import React, { PropTypes } from 'react'
import Todo from './Todo'

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

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

export default TodoList

コンポーネントをconnectするコンテナ

VisibleTodoListコンテナはTodoListコンポーネントをconnectしています。
mapStateToPropsでは、storeに格納してあるstateをpropsとして使えるようにしています。
実際には、stateすべてではなく、state.todosがtodosとして渡すようになっています。

containers/VisibleTodoList.js
import { connect } from 'react-redux'
import TodoList from '../components/TodoList'

const mapStateToProps = (state) => {
  return { todos: state.todos }
}

const VisibleTodoList = connect(
  mapStateToProps
)(TodoList)
export default VisibleTodoList

ようやくブラウザに表示

Hello Worldを表示させているだけのAppコンポーネントに
(TodoListコンポーネントをconnectした)VisibleTodoListコンテナを表示させます。

components/App.js
import React from 'react'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <VisibleTodoList />
  </div>
)
export default App

まだTodoを追加するフォームはないので、手動でtodoを追加させます。
index.jsからdispatch関数に渡すことでstateを更新します。

index.js
let store = createStore(todo)

store.dispatch(addTodo('Hello React!'))
store.dispatch(addTodo('Hello Redux!'))

スクリーンショット 2016-03-08 21.17.33.png

デバッグはReact Developer Toolsが便利です。(画面右にあるやつです。)

4. フォームからtodoを追加

フォームからtodoを追加するために、AddTodoコンポーネントを作ります。

公式ドキュメントでは、コンポーネントでもなくコンテナでもなく、
Other Componentsってなってます。
ほんとはコンポーネントとコンテナに分けてもいいんだけど、
小さすぎる場合は一緒にしてもいいんじゃない?ってことだと思います。

buttonをクリックすると、dispatch(addTodo(input.value))
inputに入ってる値をtodoに追加します。
input変数は先に宣言しておき、ref以下の関数でinput変数にinput要素を格納しています。

Reactの公式サイトReferenceには、refのコールバック属性の使い方が書かれており、
コンポーネントがマウントした直後に実行され、コンポーネント自身が
変数として格納されます。
ここでは、node変数であり、それをinput変数に格納しています。

containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'

let AddTodo = ({ dispatch }) => {
  let input

  return (
    <div>
      <input ref={(node) => {
        input = node
      }} />
      <button onClick={() => {
        dispatch(addTodo(input.value))
        input.value = ''
      }}>
        Add Todo
      </button>
    </div>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo

AppコンポーネントにAddTodoコンポーネントを追加します。

components/App.js
import React from 'react'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'

const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
  </div>
)
export default App

これで、「Add Todo」機能の完成です。
ここまでのソースコードはGitHubにあげています。

続きます。。
次回、Todoの完了・未完了を切り替える「Toggle Todo」機能を実装します。

関連記事

参考したサイト