Edited at

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」機能を実装します。


関連記事


参考したサイト