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

ReduxのAdvancedな使い方 ~ 公式ドキュメント Advanced~

はじめに

React ReduxでREST APIを叩きたいのですが,既存の記事を読んでも何をどうしたらいいのか理解できない。。。
ということで,とりあえず公式ドキュメントを読み解くことにしました。
公式ドキュメントには一通り基本的な事項は書いてあるはず。。。

ReduxのBasicsについては,Reduxの基本 ~ 公式ドキュメント Basics~で,一通り読んでまとめました。

目標

ReduxドキュメントのAdvancedを一通り理解する
とりあえず使えなくても読んでなんとなく何をしているのかを理解することを目標にします。その後,サンプルを見ながら自分で実装していこうと思います。

Advanced

この章では,基本的な事項は理解していることを前提として,AJAXやルーティングをどのように実装していくのかが説明されています。
章立ては,
1. Async Actions
2. Async Flow
3. Middleware
4. Usage with React Router
5. Example: Reddit API
6. Next Steps
となっています。本記事では,1-4.までを訳して自分なりの言葉に直していくことをしていきます。訳す際に自分の解釈を混ぜていきますので,お気をつけください。

Async Actions

ここでは,非同期アプリケーションの例題として,選択されたサブレディットの見出しを表示するReddit APIを使用しています...

...最初っからRedditってなんだ? となったので調べました。

Wikipediaによると,

reddit(レディット)は英語圏のウェブサイト。ニュース記事、画像のリンクやテキストを投稿し、コメントをつけることが可能。

となっています。英語版の2ch的なサイトだそうです。(知らなかった...)
ちなみに,サブレディットとはなんぞやもwikiに書いてあります。以下,引用。

redditでは特定の領域向けに焦点を当てたコミュニティをサブレディット(subreddit)と呼ぶ。これは匿名掲示板2ちゃんねるなどにおける『板』に相当するものであるが、ユーザーが自主的に立ち上げることが可能である。

要するに2chのような掲示板アプリを題材とすると思っておけば間違いなさそうです。

Actions

さっそく,Actionの説明から読んでいきます!

非同期APIを呼ぶとき,重要なタイミングが2つあります。
それは,

  • 呼び始めるタイミング
  • レスポンスを受け取ったタイミング

です。普通,この2つのタイミングでアプリの状態を変化させる必要があります。状態を変化させるということは,Reducerで同期的にActionをdispatchする必要があります。一般的には,どんなAPIリクエストであろうと,少なくとも以下の3種類のActionがdispatchされるはずです。

  • リクエストが始まったことをReducerに伝えるAction
    Reducerは,このActionを受け取ると,StateのisFetchingフラグをトグルします。これによって,UIがスピナーを表示するタイミングを検知できます。

  • リクエストが成功したことをReducerに伝えるAction
    Reducerは,このActionを受け取ると,isFetchingのリセットを行い,Stateに新しいデータをマージします。これによって,UIがスピナーを非表示にして,データをフェッチして表示することができます。

  • リクエストが失敗したことをReducerに伝えるAction
    Reducerは,このActionを受け取ると,isFetchingをリセットします。さらに,エラーメッセージをストアしてUIでそれを表示したりすることもできます。

Actionで,ステータスに専念するstatus filedを使用することも可能ですし,

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }

以下のように,typeで分けることも可能です。

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }

どちらを使うかは自由ですが,「複数のタイプを使うと間違いが少なくなるという」記述があります。
フラグを持つ実装でも,redux-actionsのようなヘルパーライブラリを使用してAction CreatorやReducerを生成すれば,大した問題ではなくなるようですが,チュートリアルでもタイプを分ける方法で説明が進められているところを見ると,typeを分けた方が良さそうです。

同期的なAction Creator

同期的なActionとAction Creatorを定義していきます。
まず,ユーザに選択されたサブレディットを表示するためのAction Creatorを定義します。

actions.js(Synchronous)
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'

export function selectSubreddit(subreddit) {
  return {
    type: SELECT_SUBREDDIT,
    subreddit
  }
}

単純に選択されたサブレディットを返していますね。
次に,サブレディットは,"リフレッシュ"ボタンを押すことで更新することができますので,INVALIDATE_SUBREDDITを追加します。

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'

export function invalidateSubreddit(subreddit) {
  return {
    type: INVALIDATE_SUBREDDIT,
    subreddit
  }
}

上記2つのActionは,ユーザの操作によって決定されるActionですが,ネットワークリクエストによって決定されるActionもあります。

例えば,サブレディットの投稿をフェッチするときのAction,REQUEST_POSTSがあります。

export const REQUEST_POSTS = 'REQUEST_POSTS'

function requestPosts(subreddit) {
  return {
    type: REQUEST_POSTS,
    subreddit
  }
}

リクエストを送った際のActionですね。ここでもサブレディットを返すのはなぜでしょうか。

REQUEST_POSTSアクションが,SELECT_SUBREDDITINVALIDATE_SUBREDDITとは違うアクションとして定義されていることは重要なポイントです。アプリが複雑になってくると,往々にしてユーザのアクションとは独立してデータをフェッチする必要が出てくるからです。
例えば,一番人気のサブレディットを先にフェッチするとか,古いデータを一度にリフレッシュする,といった操作をしたくなったり...といったことがよくあるようです。

また,このようにすることで,ルートの変更に応じてレスポンスをフェッチすることもできるようになります。「早い段階で特定のUIイベントに紐付けてしまうより柔軟にアプリを構築することができる」というのは,なんとなくわかります。

ネットワークのリクエストが通ってレスポンスが返ってきた場合には,RECEIVE_POSTSアクションをdispatchします。

export const RECEIVE_POSTS = 'RECEIVE_POSTS'

function receivePosts(subreddit, json) {
  return {
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

これでActionについては一通り定義できました。

※ エラーハンドリングはこの説明中では割愛されていましたが,サンプルには入っているようです。

状態の型を設計する

状態の型の設計についてですが,

  • 非同期アプリの状態を記述する情報が何なのか
  • 単一のツリーで状態を整理する方法

がわかりづらいため,初心者には混乱しやすい部分になるようです。
じっくりいきましょう。

最も一般的なユースケースであるリストを考えます(投稿のリストや友達のリストなどです)。まず,アプリが表示できるリストの種類を把握する必要があります。情報をキャッシュしたり,必要なときだけ再度フェッチしたりするのに便利なため,リスト毎に分割してStateに保存した方が良いようです。

ここで,"Redditの見出し"のためのStateの型は次のように定義することができます。

{
  selectedSubreddit: 'frontend',
  postsBySubreddit: {
    frontend: {
      isFetching: true,
      didInvalidate: false,
      items: []
    },
    reactjs: {
      isFetching: false,
      didInvalidate: false,
      lastUpdated: 1439478405547,
      items: [
        {
          id: 42,
          title: 'Confusion about Flux and Relay'
        },
        {
          id: 500,
          title: 'Creating a Simple Application Using React JS and Flux Architecture'
        }
      ]
    }
  }
}

重要なのは,以下です。

  • それぞれのサブレディットの情報を分割して保存することで,サブレディットをキャシュすることができます。ユーザがサブレディットの間の切り替えを行なうとき,瞬時にリストの更新が行えます。必要ない限り,再度フェッチする必要はありません。数万点の項目を扱っていたり,ユーザがタブを閉じることが頻繁にあったりしない限り,この情報が全てメモリに格納されていることを心配する必要はありません。

全てのサブレディットの情報をキャッシュすることで,瞬時に切替が可能であるというメリットがあるみたいですね。ただ,キャッシュするのってメモリ大丈夫なの?という心配もあると思いますが,公式ドキュメントによると大抵の場合は大丈夫なようです。(条件付きですが)

  • 各リストに対して,スピナを表示するために以下の情報を保持します
    • isFetching
    • データが残っているときに後でトグルするためのdidInvalidate
    • 最後にフェッチされたタイミングを知るためのlastUpdated
    • items自体

現実のアプリでは,fetchedPageCountnextPageUrlといったページングの状態も保存する必要がありますが,ここでは考えていないようです。

Actionの操作

上記のActionに対するReducerは以下のようになります。

reducers.js
import { combineReducers } from 'redux'
import {
  SELECT_SUBREDDIT,
  INVALIDATE_SUBREDDIT,
  REQUEST_POSTS,
  RECEIVE_POSTS
} from '../actions'

function selectedSubreddit(state = 'reactjs', action) {
  switch (action.type) {
    case SELECT_SUBREDDIT:
      return action.subreddit
    default:
      return state
  }
}

function posts(
  state = {
    isFetching: false,
    didInvalidate: false,
    items: []
  },
  action
) {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
      return Object.assign({}, state, {
        didInvalidate: true
      })
    case REQUEST_POSTS:
      return Object.assign({}, state, {
        isFetching: true,
        didInvalidate: false
      })
    case RECEIVE_POSTS:
      return Object.assign({}, state, {
        isFetching: false,
        didInvalidate: false,
        items: action.posts,
        lastUpdated: action.receivedAt
      })
    default:
      return state
  }
}

function postsBySubreddit(state = {}, action) {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
    case RECEIVE_POSTS:
    case REQUEST_POSTS:
      return Object.assign({}, state, {
        [action.subreddit]: posts(state[action.subreddit], action)
      })
    default:
      return state
  }
}

const rootReducer = combineReducers({
  postsBySubreddit,
  selectedSubreddit
})

export default rootReducer

このコードには,面白い部分が2つあります。

  • ES6を使うと,Object.assign()で簡潔にstate[action.subreddit]を更新することができること
return Object.assign({}, state, {
  [action.subreddit]: posts(state[action.subreddit], action)
})

これは次の書き方と同じものになります。

let nextState = {}
nextState[action.subreddit] = posts(state[action.subreddit], action)
return Object.assign({}, state, nextState)
  • 特定の投稿リストの状態を管理するposts(state, action)を抜き出していること

これは,Reducer Compositionの書き方です(Basicsの説明にあります)。
Reducerをどうやって小さくするかは自由に決められますが,このケースの場合は,オブジェクト内の項目の更新をposts Reducerに委譲しています。

サンプルでは,もっと進んでパラメータ化されたページネーション ReducerのためのReducer factoryを生成する方法が示してあるそうです。

注意点として,Reducerはただの関数であることは忘れないようにしましょう。そのおかげで好きなように関数の合成や,上位関数の利用が可能になっています。

非同期のAction Creator

どうやって同期的なAction Creatorをネットワークリクエストを伴う場面で使用するのか,ということがこの節で書かれています。

Reduxでこれを行なうための一般的な方法は,redux-thunkというパッケージを使用する方法です。このミドルウェアを使うことで,Action CreatorはActionオブジェクトの代わりに関数を返すことができるようになります。この方法では,Action Creatorがthunkになります。

thunkになるというのはどういうことなのかはよく分かっていませんが,要するにAction Creatorがオブジェクトではなくて関数を返すことができるようになるようです。

Action Creatorが関数を返すとき,その関数はRedux Thunk ミドルウェアによって実行されることになります。この関数は,ピュアである必要はありません。副作用があっても,非同期APIを呼んでも良いです。この関数は,先程定義した同期Actionのように,Actionをdispatchすることができるようになります。

Thunk Action creatorはactions.jsファイルで定義されます。

actions.js(Asynchronous)
import fetch from 'cross-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
  return {
    type: REQUEST_POSTS,
    subreddit
  }
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
  return {
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
  return {
    type: INVALIDATE_SUBREDDIT,
    subreddit
  }
}

// Meet our first thunk action creator!
// Though its insides are different, you would use it just like any other action creator:
// store.dispatch(fetchPosts('reactjs'))

export function fetchPosts(subreddit) {
  // Thunk middleware knows how to handle functions.
  // It passes the dispatch method as an argument to the function,
  // thus making it able to dispatch actions itself.

  return function (dispatch) {
    // First dispatch: the app state is updated to inform
    // that the API call is starting.

    dispatch(requestPosts(subreddit))

    // The function called by the thunk middleware can return a value,
    // that is passed on as the return value of the dispatch method.

    // In this case, we return a promise to wait for.
    // This is not required by thunk middleware, but it is convenient for us.

    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(
        response => response.json(),
        // Do not use catch, because that will also catch
        // any errors in the dispatch and resulting render,
        // causing a loop of 'Unexpected batch number' errors.
        // https://github.com/facebook/react/issues/6895
        error => console.log('An error occurred.', error)
      )
      .then(json =>
        // We can dispatch many times!
        // Here, we update the app state with the results of the API call.

        dispatch(receivePosts(subreddit, json))
      )
  }
}

Fetchに注意
サンプルではfetch APIが使用されています。これは,ネットワークリクエストを作成するための新しいAPIです。ほとんどのブラウザはサポートしていないので,cross-fetchライブラリを使用することが推奨されています。

// Do this in every file where you use `fetch`
import fetch from 'cross-fetch'

内部的に,クライアント側でwhatwg-fetch polyfillと,サーバ側のnode-fetchを使用することで,アプリが外部に置かれた場合でもAPI呼び出しを変更する必要がなくなるようです。(詳しい説明はなかったのでよくわかっていません)

ちなみに,polyfillとは What is a Polyfill?によると,

A polyfill, or polyfiller, is a piece of code (or plugin) that provides the technology that you, the developer, expect the browser to provide natively.

ということで,ネイティブで提供している技術を,対応していないブラウザで使うためのコードの一部や,プラグインのことのようです。

fetch polyfillは,Promise polyfillが既に存在していることを前提としています。Promise polyfillを持っていることを確認する最も簡単な方法は,BabelのES6 polyfillをエントリポイントで有効にすることです。

// Do this once before any other code in your app
import 'babel-polyfill'

dispatchメカニズムにRedux Thunk ミドルウェアを導入するために,applyMiddleware()を次のように書きます。

index.js
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()

const store = createStore(
  rootReducer,
  applyMiddleware(
    thunkMiddleware, // lets us dispatch() functions
    loggerMiddleware // neat middleware that logs actions
  )
)

store.dispatch(selectSubreddit('reactjs'))
store
  .dispatch(fetchPosts('reactjs'))
  .then(() => console.log(store.getState()))

thunkのいいところは,それぞれの結果をdispatchすることができるところです。

actions.js(fetch)
import fetch from 'cross-fetch'

export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
  return {
    type: REQUEST_POSTS,
    subreddit
  }
}

export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
  return {
    type: RECEIVE_POSTS,
    subreddit,
    posts: json.data.children.map(child => child.data),
    receivedAt: Date.now()
  }
}

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
  return {
    type: INVALIDATE_SUBREDDIT,
    subreddit
  }
}

function fetchPosts(subreddit) {
  return dispatch => {
    dispatch(requestPosts(subreddit))
    return fetch(`https://www.reddit.com/r/${subreddit}.json`)
      .then(response => response.json())
      .then(json => dispatch(receivePosts(subreddit, json)))
  }
}

function shouldFetchPosts(state, subreddit) {
  const posts = state.postsBySubreddit[subreddit]
  if (!posts) {
    return true
  } else if (posts.isFetching) {
    return false
  } else {
    return posts.didInvalidate
  }
}

export function fetchPostsIfNeeded(subreddit) {
  // Note that the function also receives getState()
  // which lets you choose what to dispatch next.

  // This is useful for avoiding a network request if
  // a cached value is already available.

  return (dispatch, getState) => {
    if (shouldFetchPosts(getState(), subreddit)) {
      // Dispatch a thunk from thunk!
      return dispatch(fetchPosts(subreddit))
    } else {
      // Let the calling code know there's nothing to wait for.
      return Promise.resolve()
    }
  }
}

これにより,コード量はほぼ同じまま,より洗練された非同期制御フローを記述することができます。

index.js
store
  .dispatch(fetchPostsIfNeeded('reactjs'))
  .then(() => console.log(store.getState()))

ThunkミドルウェアはReduxにおいて非同期Actionを統合するためのただひとつの方法というわけではなく,他にも以下の方法があります。

  • 関数の代わりにPromiseをdispathする,redux-promiseやredux-promise-middlewareを使用する方法
  • Observablesをdispatchする,redux-observableを使用する方法
  • より複雑な非同期Actionを構築するためのredux-sagaミドルウェアを使用する方法
  • promise-based asynchronous actionをdispatchするredux-packミドルウェアを使用する方法
  • サンプルのように,自分のAPIを呼び出すためのカスタムミドルウェアを書く方法

UIとの接続

非同期Actionをdispatchすることは,同期Actionをdispatchするのと変わらないので,省略されていました。詳しくはドキュメントのUsage with Reactを参照するようにとのことです。

Async Flow

ミドルウェアなしでは,Reduxのストアは同期的なデータフローしかサポートしていません。ですが,applyMiddleware()を使うことで,createStore()を拡張することができます。

redux-thunkやredux-promiseのような非同期ミドルウェアはStoreのdispatch()メソッドをラップして,Actionとは違うものをdispatchできるようにしています。例えば,関数やPromiseのようなものをdispatchできるようにしています。

どのミドルウェアもdispatchされたものをその時々で判別し,順番にチェーン内の次のミドルウェアにActionを渡すことができます。例えば,PromiseミドルウェアはPromiseを判別して,Promise毎のレスポンスに非同期的にActionの最初と最後のペアをdispatchすることができます。

チェーンの最後のミドルウェアがActionをdispatchするときには,同期的なReduxのデータフローが発生することになるので,Actionはプレーンなオブジェクトになっていなければなりません。

要するに,ミドルウェアを使うことで,いろんなものをdispatchできるようになるけど,最終的にはプレーンなオブジェクトをdispatchするようにしないといけないということですね。これは,おそらく自分でミドルウェアを書いたりするときに気をつけるべきことで,ミドルウェアを使う際は余り気にしなくても大丈夫ではないかと思います。(実際のところはわからないので覚えておくに越したことはないです)

Middleware

ここでは,ロギングとクラッシュレポートを例として,ミドルウェアとはなんぞやということを説明されています。
単純に手で実装した場合はこんなに大変だけど,middlewareを使うとこんなに簡単だよね!みたいなことが書かれています。そんなに重要ではないような感じがしたので,今回は省略します。
とりあえずmiddleware使えればいいよね。という考えです。

Usage with React Router

Reduxアプリイでルーティングをしたいときは,React Routerが使えます。Reduxはデータに対して責任を持ち,React RouterはURLにたいして責任を持ちます。大抵の場合,この2つは分割することができます。

React Routerのインストール

react-router-domはnpmからインストール可能です。

$ npm install --save react-router-dom

フォールバックURLを設定する

React Routerを統合する前に,開発サーバの設定が必要なようです。開発サーバはReact Routerの設定で宣言されたルートを知らないかもしれないので,設定をして上げる必要があります。
例えば,/todosにアクセスして更新する場合,index.htmlを提供するように指示する必要があります(SPAなので)。一般的な開発サーバでこれを有効にする方法は次のようになります。

※ create-react-appでは自動的に設定が行われるので次の手順は必要ないみたいです。

Expressの設定
index.html
app.get('/*', (req, res) => {
  res.sendFile(path.join(__dirname, 'index.html'))
})
WebpackDevServerの設定
webpack.config.dev.js
devServer: {
  historyApiFallback: true
}

ReduxアプリとReact Routerを接続する

この節では,Todosサンプルを使用して説明が行われています。(Redditは何処に行った??)

最初に,<Router /><Route />をimportします。

import { BrowserRouter as Router, Route } from 'react-router-dom'

Reactアプリでは普通,<Router />の中で<Route />をラップするらしいです。そして,URLが変更されたときに,<Router />がそのルートの先と一致したコンポーネントを描画します。このとき, <Route />は、ルートを宣言的にアプリケーションのコンポーネント階層にマップするために使用されます。

要するに,<Route />がルート先とコンポーネントを紐付けて,<Router />がそのコンポーネントを描画するということだと思います。

const Root = () => (
  <Router>
    <Route path="/" component={App} />
  </Router>
)

しかし,Reduxアプリでは,<Provider />が必要になるはずなので,その対応が次に言及されています。

ちなみに,<Provider />は,ReactにReduxをバインドできるReact Reduxによって提供される上位コンポーネントですので,次のようにimportできます。

import { Provider } from 'react-redux'

そして,ルートハンドラーがStoreにアクセスできるようにするために,<Router /><Provider />でラップします。

const Root = ({ store }) => (
  <Provider store={store}>
    <Router>
      <Route path="/" component={App} />
    </Router>
  </Provider>
)

今,<App />コンポーネントは URLが /に一致したときにレンダリングされます。さらに,URLからパラメータ:filterを読むときに必要になるので,:filter? のオプションパラメータを / につけておきます。

<Route path="/:filter?" component={App} />
components/Root.js
import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import { BrowserRouter as Router, Route } from 'react-router-dom'
import App from './App'

const Root = ({ store }) => (
  <Provider store={store}>
    <Router>
      <Route path="/:filter?" component={App} />
    </Router>
  </Provider>
)

Root.propTypes = {
  store: PropTypes.object.isRequired
}

export default Root
index.js
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import todoApp from './reducers'
import Root from './components/Root'

let store = createStore(todoApp)

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

React Routerをナビゲートする

React Routerはアプリケーションを中心としてナビゲートする<Link />コンポーネントから来ています。スタイルを追加したい場合,react-router-domはという特殊なを持ちます。これは,スタイルpropsを採用しています。例えば,activeStyleプロパティはアクティブな状態のスタイルを適用します。

サンプルでは,URLを動的に変更するために,<NavLink />を新しいコンテナコンポーネント<FilterLink />でラップしています。

containers/FilterLink.js
import React from 'react'
import { NavLink } from 'react-router-dom'

const FilterLink = ({ filter, children }) => (
  <NavLink
    to={filter === 'SHOW_ALL' ? '/' : `/${ filter }`}
    activeStyle={ {
      textDecoration: 'none',
      color: 'black'
    }}
  >
    {children}
  </NavLink>
)

export default FilterLink
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

<FilterLink />がクリックされると,URLが'/SHOW_COMPLETED''/SHOW_ACTIVE''/'の間で変更されることが確認できます。ブラウザでもブラウザ履歴を使用して,前のURLに効果的に移動することができます。

URLから読み取る

ここまででは,todoリストはURLの変更後に絞り込みが行われていません。これは,<VisibilityTodoList />mapStateToProps()stateに紐付けれており,URLには紐付いていないためです。mapStateToPropsは第二引数ownPropsを持ちます。これは,<VisibleTodoList />に渡された全てのpropsをもったオブジェクトです。

containers/VisibleTodoList.js
const mapStateToProps = (state, ownProps) => {
  return {
    todos: getVisibleTodos(state.todos, ownProps.filter) // previously was getVisibleTodos(state.todos, state.visibilityFilter)
  }
}

今,<App />には何も渡されていないので,ownPropsは空のオブジェクトです。URLに応じてtodosを絞り込むためには,<VisibleTodoList />にURLパラメータを渡す必要があります。

<Route path="/:filter?" component={App} />と書くと,これで<App />の中でparamsプロパティを入手可能になるということは前述されていました。

paramsプロパティは,URLにおいてmatchオブジェクトで特定される全てのパラメータを伴うオブジェクトです。例えば,もしlocalhost:3000/SHOW_COMPLETEDに導かれたとき,match.params{filter: 'SHOW_COMPLETED'}と同じになります。
よって,次のようにしてfilterの値を<VisibileTodoList/>に渡すことができます。

components/App.js
const App = ({ match: { params } }) => {
  return (
    <div>
      <AddTodo />
      <VisibleTodoList filter={params.filter || 'SHOW_ALL'} />
      <Footer />
    </div>
  )
}

まとめ

Redux公式ドキュメントのAdvancedを読み,まとめました。読んでみると非同期通信以外の記述も結構有りましたね。次は非同期通信やってやったぜ!という感じの記事を書けたらと思います。

参考

Redux公式ドキュメント(Advanced)
Wikipedia(Reddit)
Sharp, Remy. "What is a polyfill?". Retrieved 13 January 2012.
Reduxの基本 ~ 公式ドキュメント Basics~

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