reactとreduxを使ったExampleのソースコードリーディング:todos編

はじめに

株式会社あゆたの毛利です。 この記事は、第9回これから始める人のためのNode.js,React.js勉強会用の資料です。

ざっくりまとめただけなので、おかしなところがあったらご指摘ください。

今回のネタのreduxのexampleのtodosのソースコードはこちら

一応公式にも解説があるのですが、英語なのと私自身ソースの流れを見ないといまいち理解ができなかったので、まとめてみました。

exampleはES2015で書かれているので必要に応じてその解説もしていきます。

対象、前提

  • Javascriptが理解できている。
  • Reactがなんとなく理解できている。
  • 動かしてみる場合は、node.jsがインストールされていてnpmが使用できる環境がある。

ソースをダウンロードして動かしてみる

こちらからダウンロード(もしくはgit clone)してください。 今回読むexampleは、/examples/todosにあるので、コンソールを立ち上げディレクトリに移動してください。 その後、npm installでモジュールをダウンロードしてください。

npm install  

npm startで起動してください。

npm start  

http://localhost:3000 にアクセスして動作していることを確認してください。

処理の流れ

Reduxの概要や背景は、他所で解説されているのでググっていただくとして、ソースを読む上で把握しておくべき処理の流れはこんな感じです。

  • 最初にStateを管理するStoreを用意する。
  • ユーザ操作などイベントが発生したらActionCreatorでActionを生成する。
  • 生成したActionをdispatchする。
  • dispatchするとReducerにStateとActionが渡されるので、Stateを更新する。
  • Stateの更新に基づいてViewを更新する。

では早速ソースを読んでいきたいと思います。

/index.js

このExampleプロジェクトのエントリポイントとなるソースです。

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

let store = createStore(todoApp)

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

最初に必要なモジュールを読み込んでいます。 importは、モジュールを読み込むためのES2015の構文です。

import
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import

読み込み方が複数あってわかりづらいのですが、波カッコが付いていないのはfromの左側の変数名にfromの右側のモジュールを読み込んで代入していて、波カッコが付いている方は、波カッコの中の名称を変数名としてモジュールが持っている波カッコの中の名称のオブジェクトを代入している感じです。

このExampleでは、reactやredux以外にこの二つを連携させるreact-reduxというモジュールを使用しています。

次にlet store = createStore(todoApp)の部分でStoreを生成しています。 reduxのcreateStoreメソッドにStateを更新するための処理が記述されたReducerを渡してStoreを生成します。
letは、ブロックスコープで局所変数を宣言するときに使用するES2015の構文です。

let
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/let

次にComponentをrenderしています。 react-reduxのProviderに属性としてStoreを渡し、要素としてこのExampleで実装されているAppコンポーネントを指定しています。

componentsとcontainersディレクトリ

reactのコンポーネントがcomponentsディレクトリに、react-reduxを使って拡張されたコンポーネントがcontainersディレクトリに配置されています。

/components/App.jsルートになるコンポーネント
/components/Footer.js下部のAllやActiveなど表示のフィルタを行うリンクが列挙されている部分
/components/Link.jsFooter.jsのリンク部分のコンポーネント。FilterLink.jsでreact-reduxを使って拡張されている。
/components/Todo.jsTodoList内に表示する一つのTodoを表示するコンポーネント。
/components/TodoList.js登録されたTodoの一覧を表示するコンポーネント。VisibleTodoList.jsでreact-reduxを使って拡張されている。
/containers/AddTodo.jsTodoを登録するフォームのコンポーネントです。
/containers/FilterLink.jsFooter.jsのリンク部分のコンポーネントでLink.jsを拡張しています。
/containers/VisibleTodoList.js登録されたTodoの一覧を表示するコンポーネントでTodoList.jsを拡張しています。

/components/App.js

/index.jsでProviderに渡しているコンポーネントです。

Appコンポーネントでは、AddTodoで入力フォーム、VisibleTodoListでTodoの一覧、Footerでフィルタするリンクを表示しています。

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  

このExampleでは、Reactのコンポーネントは、Stateless functional componentsという書き方で記述されています。 Stateless functional componentsでは、引数にpropsを受け取り、レンダリングしたいDOMを返却する関数を定義することでシンプルにReactコンポーネントを定義することができます。ただし、reactのstateやライフサイクルメソッドは使用できません。

また、ここでは関数を定義する際に、ES2015のArrow Function構文で関数を定義しています。 Arrow Function
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions

const App =の後ろに記述されているカッコは引数の定義です。今回は引数がないため()になっています。=>の後ろが関数の処理や返り値になります。処理を記述する際は、通常の関数のように波カッコ内に記述してreturnしますが、返り値として値や式を指定する場合は、波カッコやreturnは省略できます。

ライフサイクルメソッドなどを使用したい場合は、ES2015のclass構文を使って定義するします。例えばこのAppコンポーネントをclassを使って記述する場合は以下のようにReact.Componentを継承してコンポーネントを作成します。

class App extends React.Component {  
  render() {
    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
      </div>
    )
  }
}

class
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

その他のES2015の構文の利用として、コンポーネントをES2015のconst宣言で定義しています。 constは読み取り専用の値を定義します。
const
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const

また、コンポーネントを外部からimportできるようにES2015のexport構文を使用しています。 https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/export

以降、Todoを登録して一覧に表示する流れとFooterのリンクで表示するTodoをフィルタする処理の流れをソースを見ながら解説していきます。

/containers/AddTodo.js

AddTodoコンポーネントでは、Todoを登録するフォームの表示とボタン押下イベントのハンドリングを行っています。

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

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

  return (
    <div>
      <form onSubmit={e => {
        e.preventDefault()
        if (!input.value.trim()) {
          return
        }
        dispatch(addTodo(input.value))
        input.value = ''
      }}>
        <input ref={node => {
          input = node
        }} />
        <button type="submit">
          Add Todo
        </button>
      </form>
    </div>
  )
}
AddTodo = connect()(AddTodo)

export default AddTodo  

下の方のAddTodo = connect()(AddTodo)ではreact-reduxを使ってReactコンポーネントを拡張しています。これにより、Reactコンポーネントに渡されるpropsにActionを実行するためのStoreのdispatchメソッドが渡されるようになります。 AddTodoもAppと同様にStateless functional componentsで定義されています。Stateless functional componentsでは引数にpropsが渡っていますが、AddTodoでは関数の引数定義が({ dispatch })のようになっており、関数内でpropsのオブジェクトを経由せずに直接dispatchにアクセスできています。
ここでは、Destructuring assignment構文が使用されています。

Destructuring assignment
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

Destructuring assignmentを使用することで、波カッコで囲われ列挙された変数に対し、代入しようとしたオブジェクトのプロパティで同じ名称のものが代入されるようになります。ここでは、引数定義にDestructuring assignmentの構文を使用することで、引数に渡ってくるprops.dispatchをdispatch変数に代入しています。

formタグでは、onSubmitにイベントハンドラを定義しています。
イベントハンドラの中では、以下のような処理を行っています。

  • preventDefaultでデフォルトのブラウザの挙動を停止
  • テキストボックスの値が空なら処理をせずに終了
  • ActionCreator(addTodo)でTodoを追加するActionを生成
  • StoreのdispatchメソッドにActionを渡す。
  • テキストボックスを空にする。

また、事前にinputタグ部分でref属性にコールバック関数を定義し、inputにアクセスできるように保持しています。

/actions/index.js

Actionを生成するための、ActionCreatorが定義されています。
ここでは、AddTodoコンポーネントで使用されているaddTodoのみ説明します。

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

処理としては、Todoの内容(text)を受け取り、Actionを返しています。 Actionにはtypeというプロパティを持たせActionの種類を識別できるようにします。それ以外の値は自由に定義します。
ここではtypeを'ADD_TODO'、idとして一意の値、textとしてTodoの内容(text)を設定してActionを生成しています。

Actionのオブジェクトのtextがプロパティ名:値のような形式になっていませんが、これはES2015のShorthand property namesの機構によってtext:textと書いたのと同義として扱われます。

Shorthand property names
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Object_initializer

/reducers/index.js

Actionがdispatchされた後は、Reducerに処理が移りますが、Stateを一つのReducerで更新するには単位が大きいため、Stateの内部のプロパティごとに担当するReducerを分けます。
このソースでは、reduxのcombineReducersメソッドを使って、Stateの各プロパティを更新するReducerを登録しています。

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

const todoApp = combineReducers({  
  todos,
  visibilityFilter
})

export default todoApp  

ここでcombineReducersに渡しているオブジェクトは前述のShorthand property namesが使われており、以下の定義と同義になります。

const todoApp = combineReducers({  
  todos:todos,
  visibilityFilter:visibilityFilter
})

Stateのtodosプロパティを更新するのがtodos.jsに定義されているReducer、StateのvisibilityFilterプロパティを更新するのがvisibilityFilter.jsに定義されているReducerの役割になります。

combineReducersで生成されたオブジェクトとStoreを紐付ける処理は/index.jsのcreateStore(todoApp)で行っています。

/reducers/todos.js

Stateのtodosプロパティの更新を担当するReducerです。Todoの一覧を処理する部分とTodo一つを処理する部分に分かれているため、最初にTodoの一覧を処理するtodos関数を説明していきます。

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

Reducerは第一引数にstate、第二引数にactionをとる関数として定義します。
/reducers/index.jsのcombineReducersで設定したため第一引数のstateはStoreが保持しているStateのtodosプロパティです。 第二引数のactionはdispatchされたActionです。 処理としては、action.typeで処理を分岐し、元のstateの値とactionで渡された値をから新しいstateを生成して(変更がない場合は元のstateのまま)返します。 今は、Todoの追加のが流れを追っているのでADD_TODOの処理のみ着目します。

return [  
  ...state,
  todo(undefined, action)
]

...stateの部分は、引数で受け取った配列をES2015のSpread Operator文で展開して配列リテラルで定義し直し一番最後にtodo関数で新規に作成したTodoオブジェクトを追加しています。

Spread Operator
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_operator

処理としては、元のstate(Todoの一覧)に新しいTodoを追加する処理なのですが、pushメソッドを使ってTodoを追加しないのは、ReducerでのStateの変更は、元のStateの値を差し替えるのではなく、新しくオブジェクト(や配列)としてStateを生成して返すルールになっているためです。

あと、Reducerの引数のstateがstate=[]のように、値がなかった時に初期化されるように定義されていますがこちらもES2015の機能になります。

Default Parameters
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functionsandfunctionscope/Defaultparameters

次にTodoの一覧の中の一つ一つのTodoの処理を行うtodo関数について見ていきます。

const todo = (state, action) => {  
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text,
        completed: false
      }
    case 'TOGGLE_TODO':
      if (state.id !== action.id) {
        return state
      }

      return Object.assign({}, state, {
        completed: !state.completed
      })
    default:
      return state
  }
}

todo関数もstateを更新するReducerとして同様の作りになっており、state,actionを引数に取り、action.typeで処理を分岐し、stateを更新する場合は新しいオブジェクトとしてstateを返しています。
action.typeがADD_TODOの時は、新しいTodoオブジェクトを生成してstateとして返しています。

case 'ADD_TODO':  
  return {
    id: action.id,
    text: action.text,
    completed: false
  }

/containers/VisibleTodoList.js

Stateが変更されたことによってReactで画面の再描画を行う処理に入っていきます。
描画処理自体は、TodoList.jsやTodo.jsで行いますが、reactとreduxの処理を関連づけるためにVisibleTodoList.jsでTodoList.jsを拡張しています。

拡張方法はreact-reduxのconnectメソッドの第一引数に、Stateの値を元にreactコンポーネントにpropsとして渡す値を返すコールバック、第二引数に、イベントハンドラ内でdispatchを行うコールバックを指定します。

第一引数に渡すmapStateToPropsでは、Todoの一覧(state.todos)をgetVisibleTodosでフィルタリングしてtodosというプロパティ名でpropsに渡るようにオブジェクトを返しています。

第二引数に渡すmapDispatchToPropsでは、Todoをクリックした時にTOGGLE_TODOのActionをdispatchするコールバック関数をonTodoClickというプロパティ名でpropsに渡るようにオブジェクトを返しています。

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

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

const VisibleTodoList = connect(  
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

/components/TodoList.js

TodoList.jsでは実際にTodo一覧を描画する処理を行っています。
TodoListコンポーネントでは、VisibleTodoList.jsで拡張されたtodos, onTodoClickをpropsとして受け取っています。

表示するTodoの一覧であるtodos(配列)のmapメソッドで配列内の各要素にアクセスし、Todoコンポーネントで描画しています。{...todo}はtodoオブジェクト内の各プロパティを属性=値の組み合わせでコンポーネントに渡しています。onClick属性にはTodoコンポーネント内でイベントハンドラとして使用するコールバック関数を渡しています。

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

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

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

export default TodoList  

/components/Todo.js

Todoコンポーンネントでは、Todoをliタグで描画します。

import React, { PropTypes } from 'react'

const Todo = ({ onClick, completed, text }) => (  
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)

Todo.propTypes = {  
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default Todo  

TOGGLE_TODO Actionの流れ

登録されたTodoはクリックすると打ち消し線で完了状態になります。この処理の流れは、以下のようになります。

Todo.jsのonClick={onClick}部分でliタグにクリック時のイベントハンドラとしてコールバック関数を設定しています。このコールバック関数はTodoList.jsから渡されています。

TodoList.jsのonClick={() => onTodoClick(todo.id)}部分でTodoList.jsでイベントハンドラとして設定するコールバック関数を渡しています。onTodoClickは、VisibleTodoList.jsでreact-reduxのconnect関数で拡張しています。

VisibleTodoList.jsでeact-reduxのconnect関数の第二引数にmapDispatchToPropsを渡すことで、TodoListコンポーネントにActionをdispatchするコールバック関数を渡しています。
onTodoClickコールバック関数の中では、toggleTodo ActionCreatorを呼び出しActionをdispatchしています。

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

actions/index.jsにTOGGLE_TODOのためのActionCreatorが定義されています。

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

このActionがdispatchされると、ADD_TODOの時と同様にReducerが実行されます。

ADD_TODOはStateのtodosの一要素を更新するするため、combineReducersにtodosとして登録されているtodos.jsのReducer(todos関数)で処理されています。

todos.jsのtodos関数でaction.typeがTOGGLE_TODOの時の処理は、以下のようになっています。state(Stateのtodos配列)のmap関数で各Todoに対してtodos.jsのtodo関数を呼び出しています。

case 'TOGGLE_TODO':  
  return state.map(t =>
    todo(t, action)
  )

todos.jsのtodo関数でaction.typeがTOGGLE_TODOの時の処理は、以下のようになっています。stateとして渡されているtodoのidが同じもののみcompletedプロパティの真偽値の値を逆転させています。

Stateを新しく作るために使用されているObject.assignは、ES2015で実装されたメソッドです。
Object.assign()
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

Object.assignを使用している理由は、ADD_TODOの処理の流れの時に触れたように、元のStateの値を直接書き換えず新しいオブジェクトとしてStateを生成するためです。

case 'TOGGLE_TODO':  
  if (state.id !== action.id) {
    return state
  }

  return Object.assign({}, state, {
    completed: !state.completed
  })

Stateが変更されるとTodoを追加した流れと同じようにReactのコンポーネントが再描画されます。
最終的にTodo.jsのliタグで指定している以下の部分により打ち消し線の表示非表示が切り替わるようになります。

style={{  
  textDecoration: completed ? 'line-through' : 'none'
}}

/components/Footer.js

次にTodo一覧のフィルタの処理を見ていきます。 Footerコンポーネントでは、FilterLinkコンポーネントを使ってフィルタのリンクを描画しています。FilterLinkには、filter属性でフィルタの種別(SHOW_ALL,SHOW_ACTIVE,SHOW_COMPLETED)を渡しています。

import React from 'react'  
import FilterLink from '../containers/FilterLink'

const Footer = () => (  
  <p>
    Show:
    {" "}
    <FilterLink filter="SHOW_ALL">
      All
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_ACTIVE">
      Active
    </FilterLink>
    {", "}
    <FilterLink filter="SHOW_COMPLETED">
      Completed
    </FilterLink>
  </p>
)

export default Footer  

/containers/FilterLink.js

FilterLinkコンポーネントはLinkコンポーネントを拡張しています。
mapStateToPropsでは、Footerコンポーネントで指定しているfilter属性とstateのvisibilityFilterが同じかどうかをactiveプロパティとしてpropsに渡すように定義されています。

mapDispatchToPropsでは、リンクをクリックした時にActionCreatorであるsetVisibilityFilter関数を使って、SET_VISIBILITY_FILTERのActionを生成し、dispatchしています。

import { connect } from 'react-redux'  
import { setVisibilityFilter } from '../actions'  
import Link from '../components/Link'

const mapStateToProps = (state, ownProps) => {  
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

const mapDispatchToProps = (dispatch, ownProps) => {  
  return {
    onClick: () => {
      dispatch(setVisibilityFilter(ownProps.filter))
    }
  }
}

const FilterLink = connect(  
  mapStateToProps,
  mapDispatchToProps
)(Link)

export default FilterLink  

actions/index.js

SET_VISIBILITY_FILTERのActionCreatorは以下のようになっています。引数として受け取った値を単純にActionのfilterプロパティとして持たせています。

export const setVisibilityFilter = (filter) => {  
  return {
    type: 'SET_VISIBILITY_FILTER',
    filter
  }
}

/reducers/visibilityFilter.js

SET_VISIBILITY_FILTERは、visibilityFilter.jsのReducerで処理しています。/reducers/index.jsでcombineReducersにvisibilityFilterという名前で登録しているので、Storeが保持するStateのvisibilityFilterプロパティの更新を担当するReducerになります。
処理は単純で、action.typeがSET_VISIBILITY_FILTERだったらactionのfilterをstateとして返しています。

const visibilityFilter = (state = 'SHOW_ALL', action) => {  
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

export default visibilityFilter  

/components/Link.js

FilterLink.jsの拡張によりstateとコンポーネントの属性で指定された値が同じかどうかが真偽値としてprops.activeに代入されて渡ってきます。props.activeの場合は普通に文字列として表示し、activeでない場合は、リンクとして表示してFilterLinkコンポーネントで定義したonClickをイベントハンドラで実行するようにしています。

import React, { PropTypes } from 'react'

const Link = ({ active, children, onClick }) => {  
  if (active) {
    return <span>{children}</span>
  }

  return (
    <a href="#"
       onClick={e => {
         e.preventDefault()
         onClick()
       }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {  
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

export default Link  

/containers/VisibleTodoList.js

フィルタの値が変更されたので、一覧の表示を変更する必要があります。 VisibleTodoListのmapStateToPropsでtodosにTodoの一覧をセットする際にgetVisibleTodos関数を使って表示するTodoのフィルタリングを行っています。
TodoListコンポーネントにフィルタリングされたTodo一覧が渡されることで再描画されます。

const getVisibleTodos = (todos, filter) => {  
  switch (filter) {
    case 'SHOW_ALL':
      return todos
    case 'SHOW_COMPLETED':
      return todos.filter(t => t.completed)
    case 'SHOW_ACTIVE':
      return todos.filter(t => !t.completed)
  }
}

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

まとめ

ざっくりでしたが、処理の流れは読めたと思います。

ポイントまとめてみました。

  • 最初にStateを管理するStoreを用意する。Stateは一つのオブジェクトとして管理される。(/index.js)
  • Providerでルートになるコンポーネントをラップしreactのrenderを呼び出す。(/index.js)
  • ユーザ操作などイベントが発生したらActionを生成する。Actionは、typeプロパティにActionのタイプを文字列で持たせる。その他の値は自由に持たせる。(/actions/index.jsとActionCreatorの参照元)
  • 生成したActionをdispatchする。dispatchするとReducerが実行される。(ActionCreatorの参照元)
  • Reducerには、StateとActionが渡されるので、その情報に基づいて新しいStateを生成する。(/reducersディレクトリ内のソース)
  • combineReducersを使ったりネストしたりして、Reducerを細分化してStateをプロパティ単位で担当させる。(/reducersディレクトリ内のソース)
  • Stateの更新に基づいてコンポーネントが再描画される。その際、Stateの紐付けやStoreのdispatchをコンポーネントから呼び出せるようにするためにreact-reduxのconnect関数を使用する。(/components,/containersディレクトリ内のソース)

もう少し理解が進んだら加筆、修正していきたいと思います。