LoginSignup
26
26

More than 5 years have passed since last update.

Reduxの基本 ~ 公式ドキュメント Basics~

Last updated at Posted at 2018-01-03

はじめに

Reactを本格的に実装するためには,Reduxという代物を使うと良いらしいということを知り,実装したくてRedux調べてたけど一向に理解できない。。。
ということで,とりあえず公式ドキュメントを読み解くことにしました。

ちなみに,公式ドキュメントのBasicsの章のはじめにこうあります。

Don't be fooled by all the fancy talk about reducers, middleware, store enhancers—Redux is incredibly simple. If you've ever built a Flux application, you will feel right at home. If you're new to Flux, it's easy too!

少なくとも今のところ,そうは感じられない。。。(能力不足)
ということで目標は以下。

目標

ReduxドキュメントのBasicsを一通り理解する
自分なりの解釈が間違ってるところとか出るかもしれないので悪しからず。。。

Basics

ドキュメントのBasicsの章では,シンプルなToDoアプリを題材に,
1. Actions
2. Reducers
3. Store
4. Data Flow
5. Usage with React
6. Example: Todo List
の説明がされています。今回は1-5までをまとめようと思います。

Actions

Actionとは,アプリからStoreに対して送るデータのこと。
新しいtodoの項目を追加するためのアクションは,

const ADD_TODO = 'ADD_TODO'
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

という感じで書けます。ActionはJavaScriptの一般的なオブジェクトで,typeは必須プロパティ。
Actionの構造は好きにして定義して良いらしいです。ただ,Actionの中身(データ)はできるだけ小さい方が良くて,例えば,todoを完了するためのトグル用のActionは

{
  type: TOGGLE_TODO,
  index: 5
}

こんな感じで,todoオブジェクト自体を入れるのではなくて, index だけを入れたりして小さくなるようにしたほうが良い。

todoリストの表示非表示を制御するためのactionは,

{
  type: SET_VISIBILITY_FILTER,
  filter: SHOW_COMPLETED
}

こうなります。

Action Creators

Action Creatorとは,文字通りactionを生成する関数のこと。
ActionとAction Creatorは混同しやすいので気をつけましょう。

Action Creatorは次のようにActionを返すだけのシンプルな関数にします。

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

ちなみに,Fluxでは次のようにAction Creator中でdispatchすることがよくあったらしい。

function addTodoWithDispatch(text) {
  const action = {
    type: ADD_TODO,
    text
  }
  dispatch(action)
}

でも,Reduxではこんな書き方はせずに,dispatch()関数にActionの結果を渡すようにします。

dispatch(addTodo(text))
dispatch(completeTodo(index))

これをラップしてBound Action Creatorを作ることもできます。

const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))

こうすると,

boundAddTodo(text)
boundCompleteTodo(index)

こんな感じで直接dispatchすることができます。

dispatch()関数はstore.dispatch()としてstoreから直接触ることもできるけど,react-reduxのconnect()みたいなhelperを使ってアクセスするのが普通のようです。

bindActionCreators()を使って,自動的にAction creatorをdispatch()関数に接続することもできます。これは,Action Creatorが多くなったときに便利らしいですが,Advancedな用法ということなので,今回は割愛します。

Reducers

Actionからは何が起きたのかという事実を知ることができます。ですが,アプリの状態(State)がどのように変化するのかということはわかりません。これを担うのが,Reducerです。

Stateの型を設計する

Reduxでは,全てのアプリの状態をひとつのオブジェクトとして保持します。この仕様のおかげで,コードを書く前に状態の持ち方を考えやすくなっているそう。

例えば,todoアプリの例だと,次の2種類の状態を保持したい。

  • 表示/非表示のフィルタ
  • todoリスト

UIの状態から保持するデータを決めがちですが,UIの状態とデータは出来るだけ分けて考えたほうが良いようです。

{
  visibilityFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Consider using Redux',
      completed: true,
    },
    {
      text: 'Keep all state in a single tree',
      completed: false
    }
  ]
}

補足:
Stateはネストせず,出来るだけ正規化したほうが良いです。ストアする全てのオブジェクトはkeyとしてIDを持つようにして,他のエンティティからはできるだけIDを参照するようにします。アプリのstateはDBのように考えると良いようです。

Actionの操作

ここまでで,オブジェクトのStateが決まったので,Reducerを書きます。
Reducerは,前のStateとActionを持ち,次のStateを返す関数で,Actionの操作を司ります。

最初に,Reducerでしてはいけないことが記述されていました。

  • 入力値の変更
  • API呼び出しやルーティングの移行などの副作用を伴う操作
  • ピュアじゃない関数の呼び出し(例:Date.now()Math.random()

要するに,同じ入力が与えられたときに,次のStateを返すだけに留めるべきで,副作用が起きるようなAPIコールや,入力値の変更などはするべきではないということのようです。

Reduxは最初にStateをundefinedとしてReducerをコールします。このときに,Stateの初期化を行うようにします。

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {
  // アクションの操作はしない
  // 与えられたStateをただ返す
  return state
}

次に,SET_VISIBILITY_FILTERを操作してみます。ここではvisibilityFilterを変更するだけ。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    default:
      return state
  }
}

ここで,

  • stateを変更していないこと
  • デフォルト値として前のstateを返していること

に注意!Object.assign()のところは,オブジェクトの展開を使って{ ...state, ...newState}というようにも書くこともできます。

複数のActionを操作する

複数Actionの操作の例は,

import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} from './actions'

...

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

となります。ここでは,ADD_TODOTOGGLE_TODOをimportして, ReducerにはADD_TODOの操作を記述しています。

ここでも,stateやそのフィールドを直接書かずに,代わりに新しいオブジェクトを返しています。この新しいオブジェクトは,古いtodosオブジェクトに新しいtodosを加えたオブジェクトになっています。
新しいtodoはActionのデータを使って,追加されていますね。

最後に,TOGGLE_TODOの操作の実装です。

case TOGGLE_TODO:
  return Object.assign({}, state, {
    todos: state.todos.map((todo, index) => {
      if (index === action.index) {
        return Object.assign({}, todo, {
          completed: !todo.completed
        })
      }
      return todo
    })
  })

既存のActionを変更せずに,配列の特定の項目を更新したいので,該当するindexの項目以外の同じ項目の新しい配列を生成しています。
もし,この操作を頻繁に書くようであれば,immutablitiy-helper,updeepのようなヘルパーや,Immutableのようなライブラリを使うと良いよいです。この辺りはとりあえず割愛します。

また,クローンするまえにstateの中身に何かを勝手に割り当てるとかそういうことはしてはいけません。あくまでクローンしたオブジェクトに関して変更を加えます。

Reducerの分割

まず,Reducerのコードの全容を。

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return Object.assign({}, todo, {
              completed: !todo.completed
            })
          }
          return todo
        })
      })
    default:
      return state
  }
}

todosvisibilityFilterは完全に独立して更新されています。
往々にして,Stateフィールドは他のフィールドに依存していたりして,考慮が必要だったりするようです。
ただ,今回の場合は簡単にtodosを別関数に分割することができます。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

todosstateを受け取っていますが,これは配列であることに注意!
今,todoApptodosにstateの一部分を渡し,todosがそれを更新します。
これは,reducer compositioin と呼ばれ,Reduxアプリの基本的なパターン のようです。

要は,共通するオブジェクトを返す部分は,それだけで抜き出して分割できるってことですね。

次に,visibilityFilterを管理するReducerの抜き出し方を見てみます。
まず, SHOW_ALLを宣言します。

const { SHOW_ALL } = VisibilityFilters

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

メインのReducerを,Stateを部分的に管理するReducer達を呼んで,ひとつのオブジェクトで結びつける関数として書き換えることができます。
完全な初期状態を知る必要はもはやなく,子Reducerは最初,undefinedを与えられたときの初期状態(defaultの返り値)を返せば良くなります。

function todos(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

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

function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

これらのReducerはグローバル状態の一部だけを管理していることに注意してください。各関数に渡されるstateはReducer毎に違います。

このようにすると,アプリが大きくなったときにReducerを別ファイルに分割し,各ファイルを完全に独立させて違うデータ領域を管理することができます。

ReduxはcombineReducers()というユーティリティを提供していて,これは,上記のtodoAppと同じ定型ロジックを実行します。そのため,todoAppcombineReducers()を使って,書き直すことができます。

import { combineReducers } from 'redux'

const todoApp = combineReducers({
  visibilityFilter,
  todos
})

export default todoApp

combineReducers()を使わずに,次のようにも書けます。

export default function todoApp(state = {}, action) {
  return {
    visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    todos: todos(state.todos, action)
  }
}

また,違うキーを使ったり,違う関数を呼ぶことも可能です。この2つのcombinedReducerの書き方は同じ動作をします。

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}

上の例のcombineReducers()は全て,キーに対応して選択されたstateの一部を伴うReducerを読んで,その結果をひとつのオブジェクトに結合しています。
combineReducers()は,Reducerがstateを変更しない限り,新しいオブジェクトを生成しません。

個人的には,combineReducers()を使うほうが見やすいし理解しやすいと思います。他の方の記事を見ていてもcombineReducers()を使う例が多く,これを使うのが自然な気がしています。

Store

ここまでで,

  • 何が起きるのかついての事実を表現するAction
  • Actionに応じてstateを更新するReducer

を定義しました。

Storeは上記2つをつなげるオブジェクトになります。Storeは以下のような役割を担っています。

  • アプリのstateを保持する
  • getState()でstateへのアクセスを許可する
  • dispatch()でstateを更新することを許可する
  • subscribe(listener)でリスナーを登録する
  • subscribe(listener)で返される関数を通して,リスナーの登録解除を行なう

Reduxアプリ内ではただひとつのStoreを持つということは重要な注意事項なので忘れないようにしましょう!

データ操作のロジックを分割したいときには,Storeをたくさん使う代わりに,reducer compositionを使って分割します。

Reducerを既に準備していたら,storeを作るのは簡単です。
combineReducers()でまとめたReducerをimportして,createStore()に渡すだけです。

import { createStore } from 'redux'
import todoApp from './reducers'

let store = createStore(todoApp)

第2引数で初期状態を設定することも可能です。
これはサーバ上で実行しているReduxアプリのstateとクライアントのstateを一致させるのに役立ったりするようです。

let store = createStore(todoApp, window.STATE_FROM_SERVER)

とりあえずこういうのもあるよ。ということですね。必要になったら再度調べよう。。。

Actionをdispatchする

Storeを生成したら,プログラムが動くことを確認してみましょう。実行の仕方は以下のようにします。UIがなくても,更新ロジックをテストすることができます。

import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// Log the initial state
console.log(store.getState())

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// Stop listening to state updates
unsubscribe()

Reduxを使うと,全てがピュアな関数で表現されることになるので,UIを書くことなくアプリの挙動を確認できます。だから,テストも書きやすいんですね。ただ関数をコールして,アウトプットを確認するだけ。いいですね。

Data Flow

Reduxアーキテクチャは、単一方向の厳密なデータフローを中心に展開されます。
どういうことかというと,アプリ内のデータは全て,同じライフサイクルのパターンに従うので,アプリのロジックはより理解しやすいものになります。予想外のことをしたりすることはできないので,結果的にわかりやすくなるはずであるということのようです。
また,こうすることで,データの正規化が促進され,相互に認識されない同一データのコピーが作成されることはなくなります。

つまりは,単一方向のデータフローにすることで,データフローを追いやすく,不要なデータのコピーとかが作成されにくくなっているようです。

Reduxアプリのデータライフサイクルは以下の4つのステップに従います。

1. store.dispatch(action)を呼ぶ
Actionは何が起こったのか,だけを説明する一般的なオブジェクトです。Storeに対するdispatch()はアプリ内のどこからでも呼び出し可能になります。

 { type: 'LIKE_ARTICLE', articleId: 42 }
 { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
 { type: 'ADD_TODO', text: 'Read the Redux docs.'

2. Reduxのstoreは,reducer関数を呼ぶ
Storeは,現在のStateツリーとActionの2つをReducerに渡します。例えば,todoアプリにおいて,Root reducerは次のようになります。

// The current application state (list of todos and chosen filter)
let previousState = {
  visibleTodoFilter: 'SHOW_ALL',
  todos: [
    {
      text: 'Read the docs.',
      complete: false
    }
  ]
}

// The action being performed (adding a todo)
let action = {
  type: 'ADD_TODO',
  text: 'Understand the flow.'
}

// Your reducer returns the next application state
let nextState = todoApp(previousState, action)

Reducerはピュアな関数であることに注意してください!Reducerはただ次のStateを計算しているだけです。Reducerに対して,同じ入力をすると,いくらやっても同じ出力しか出てきません。
APIコールやルーターの遷移のような副作用を起こすことはありません。ただ,次のStateを返すだけです。APIコールのような操作は,アクションがdispatchされる前に行われるべき操作です。

3. Root reducerは,複数のreducerの出力をひとつのStateツリーに結合する
どのようにRoot reducerを構成するかは自由に決めて良いようです。
Reduxはreducerを分割して管理するためのcombineReducers()というヘルパー関数を持っているので,これを使うことが推奨されています。書き方は以下のようになります。

function todos(state = [], action) {
   // Somehow calculate it...
   return nextState
 }

 function visibleTodoFilter(state = 'SHOW_ALL', action) {
   // Somehow calculate it...
   return nextState
 }

 let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 })

Actionを実行するとき,combineReducers()によって返ってきたtodoAppは両方のreducer呼びます。

 let nextTodos = todos(state.todos, action)
 let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)

これは2つの結果をひとつのstateツリーに結合します。

 return {
   todos: nextTodos,
   visibleTodoFilter: nextVisibleTodoFilter
 }

どうやら,関数の名前がオブジェクトのkeyになっているようですね。

4. ReduxのStoreは,Root reducerから返ってきた完全なStateツリーを保存する
新しいツリーはアプリの次のStateになります。このとき,store.subscribe(listener)で登録された全てのリスナーが呼び出されることになります。リスナーが,現在のstateを取得するときにはstore.getState()を呼んで取得します。

このタイミングで,新しいStateを反映するためにUIが更新されます。React Reduxのようなバインディングを使用しているときは,component.setState(newState)が呼ばれ,更新されることになります。

Reactとの併用方法

ReduxはReactに依存しているわけではないため,Angular,Ember,jQuery,vanilla JavaScriptなどで使用することができます。ただ,ReactとDekuはstateの関数としてUIを記述できるためReduxと相性が良いといえます。ReduxはActionに応じてStateの更新を行います。

react-reduxのインストール

react-reduxを使用するためnpmでインストールします。npmを使わない方法もありますが割愛します。

$ npm install --save react-redux

Presentational and Container Components

Reduxを使用したReactには,

  • Presentational components
  • Container components

の2つのコンポーネントがあります。

Container Componentsが,Reduxを使ったコンポーネントになります。
ほとんどのコンポーネントはPresentationalになります。一般的なReactのコンポーネントと思ってもらって良いです。ReduxのstoreをPresentational Componentsに紐付けるためにContainer Componentsを生成する必要があります。
もし,Container Componentsが複雑になりすぎる(例えば,Presentional Componentsがたくさんネストしていて,下位に渡されるコールバックが大量にある)場合,コンポーネントツリー内のもう一つのコンテナを導入した方が良いです。

store.subscribe()を使えば手でContainer Componentsを書くこともできますが,React Reduxは手で書いて最適化するのは難しいみたいなので辞めておいた方が良さそうです。
connect()関数を使ってContainer Componentsを生成することが推奨されています。

コンポーネントの階層を設計する

コチラに関してはドキュメントでも他記事参照となっていたので割愛します。
(Thinking in React)[https://reactjs.org/docs/thinking-in-react.html]を参照。

Presentational Componentsを設計する

Presentational Componentsと,その中身の説明を簡単に列挙すると下のようになります。

  • TodoList は表示するtodosのリスト
    • todos: Array はtodoの項目の配列 {id, text, completed}
    • onTodoClick(id: number) はtodoをクリックしたときに発火するコールバック
  • Todo はtodoの項目
    • text: stringはテキスト
    • completed: booleanは表示するかどうか
    • onClick()はtodoがクリックされたときに発火するコールバック
  • Linkはコールバックを伴うリンク
    • onClick()はクリックされたときに発火するコールバック
  • Footerは表示されているtodosをユーザに変更させる場所
  • Appはルートコンポーネント

これらは,見た目を描画しますが,どこからデータが来るのかや,どうやってデータを変更するかは知りません。ただ与えられたものをレンダーするだけです。
あるものがReduxから渡されたとき,コンポーネントは常に同じものになります。

Container Componentsの設計

Container Componentsは,Presentational ComponentsをReduxに接続するために必須です。
例えば,PresentationalなTodoListコンポーネントは,ReduxのStoreを定期的にみて,現在のvisibility filterを適用するVisibleTodoListのようなコンテナを必要とします。また,visibility filterを変更するために,適切なクリックアクションをdispatchするLinkをレンダーするFilterLinkコンテナコンポーネントも必要です。
まとめると,

  • VisbleTodoListは現在のvisibility filterに応じてtodosを絞り込み,TodoListを描画する
  • FilterLinkは現在のvisibility filterを取得し,Linkを描画する
    • filter: stringは表示するvisibility filter

他のコンポーネントを設計する

コンポーネントを必ず,Presentationalか,Containerかに区別しなければならなないわけではありません。例えば,コンポーネントが小さく,formと関数が一緒になっている場合などがあります。

  • AddTodoはAddボタンを伴うinputフィールド

技術的に分割することはできるけど,分割するほどじゃない場合は,両者を一緒に書いてしまったほうがよいかもしれません。これが大きくなってくると,どのように分ければいいかが自ずと見えてくるようです。

Presentational Componentsの実装

これは普通のReactコンポーネントなので,Reduxのドキュメントでは詳細には触れられていませんでした。それぞれのコンポーネントを下に貼っておきます。単純なReactのコンポーネントです。

components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'

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
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map((todo, index) => (
      <Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
    ))}
  </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/Link.js
import React from 'react'
import PropTypes from 'prop-types'

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
components/Footer.js
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

Container Componentsの実装

技術的には,Container Componentsは,ReduxのStateツリーの一部分を読んだり,描画するPresentational Componentsのプロパティを供給するためのReactコンポーネントである。Container Componentsの中で,store.subscribe()を使ってPresentational ComponentsとReduxを接続することになる。
だが,Container ComponentsはReact Redux ライブラリのconnect()関数を使うことが推奨されている。これを使うことによって,Reactのレンダリングが最適化されようです。

connect()を使うためには,現在のReduxのStoreのStateを,Presentational Componentsに渡すプロパティ場合に変換する必要がある。この方法を示しているのが,mapStateToPropsという関数で,これを定義しないといけないようです。
例えば,VisibleTodoListTodoListに渡すためのtodosを計算する必要があるので,state.visibilityFilterに対応するstate.todosを絞り込む関数を定義します。そして,mapStateToPropsでそれを使用する。

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

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

さらにStateを読むことに加えて,Container ComponentsはActionをdispatchすることができます。dispatch()メソッドを受け取って,Presentational Componentsに挿入したいpropsのコールバックを返すmapDispatchToProps()という関数を定義することができます。

例えばTodoListコンポーネントにonTodoClickというプロパティを挿入するためには,VisibleTodoListと,TOGGLE_TODOアクションをdispatchするonTodoClickが必要になります。

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

最後に,connect()を呼び,これらの2つの関数を渡すVisibleTodoListを生成します。

import { connect } from 'react-redux'

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

export default VisibleTodoList

これらは,React redux APIの基本ですが,もっと簡単に記述する方法があるので,気になる方はドキュメントを参照。
残りのコンテナコンポーネントの書き方は以下のようになります。

containers/FilterLink.js
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
containers/VisibileTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'

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)
  }
}

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

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

export default VisibleTodoList

他のコンポーネントの実装

containers/AddTodo.js
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

refとかよく分かってないけど今回は飛ばします。こう書けばいいらしいということはわかる。

コンポーネント内でコンテナを結びつける

components/App.js
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

Storeの渡し方

Container Componentsは,ReduxのStoreにアクセスする必要があるので,Storeにsubscribeすることができる。全てのコンテナコンポーネントにpropとして渡す方法もありますが,それは何かと面倒らしいです。Presentational Componentsのコールバックのネストが深くなったりしたときにいちいち手で書かないといけなかったりするらしい。

それを解決するために,<Provider>を使用して接続することが推奨されているようです。

index.js
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')
)

まとめ

以上で,Basicsの記述は一通り読むことができました。
Container Componentsを書いて,ReactのPresentational Componentsに対してStoreの情報を渡せるんですね。このあたりは実装しながら理解を深められたらと思います。

参考文献

Redux公式ドキュメント

26
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
26