Reduxの公式ExampleにあるTodo Listを少しづつ書いてみました。
最終的な動作イメージは以下のような感じです。
Github Pagesにもあげていますので、触ってみてください。
基本的なReduxの説明や環境構築などは他のドキュメントに譲るとして、
ここでは各機能ごとに動くものを作りながら、最終的にExampleの形になることを目指します。
Reactがだいたい分かってて(チュートリアルをやったくらい)で、
Reduxを触り始めたくらいの人を対象としています。
Reduxを触り始めて日が浅いので、間違いもあるかと思います。ぜひご指摘ください。
ExampleのTodo Listの機能は次の3つです。
- TodoをTodo Listに追加する「Add Todo」
- Todoの完了・未完了を切り替える「Toggle Todo」
- 表示する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のコンポーネントをラップしており、
これが最上位になります。
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を表示させているだけです。
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に渡されます。
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を返します。
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は消えるはずです。
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が更新されるのを見てみます。
- actionCreatorであるaddTodo('Hello World!')で{ id: 0, text: 'Hello World' }というactionが作成される。
- store.dispatch関数にactionを渡すことで、todo reducerにaction = { id: 0, text: 'Hello World' }, state = 現在のstate(初期値を与えてないのでundefinedだと思います。)
- todo reducerでは、新しいstate = { id: 0, text: 'Hello World' }を返します。
storeが保持しているstateはstore.getStateで取得できます。
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関数が呼び出されます。
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]}といった形で保持されます。
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を表示するだけです。
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}と同じです。
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として渡すようになっています。
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コンテナを表示させます。
import React from 'react'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<VisibleTodoList />
</div>
)
export default App
まだTodoを追加するフォームはないので、手動でtodoを追加させます。
index.jsからdispatch関数に渡すことでstateを更新します。
let store = createStore(todo)
store.dispatch(addTodo('Hello React!'))
store.dispatch(addTodo('Hello Redux!'))
デバッグは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変数に格納しています。
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コンポーネントを追加します。
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」機能を実装します。
関連記事
- Redux ExampleのTodo Listをはじめからていねいに(1) -> この記事
- Redux ExampleのTodo Listをはじめからていねいに(2)
- Redux ExampleのTodo Listをはじめからていねいに(3)