Help us understand the problem. What is going on with this article?

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

関連記事

参考したサイト

xkumiyu
Data Scientist
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした