Posted at

Redux公式サイトの Redux Advanced を翻訳しながらやってみた。 Async Actions編

More than 1 year has passed since last update.


Redux Advance

redux advancedをやろうとしたとき翻訳しているサイトを見つけることができなかったので翻訳していきながらやっていきたいと思います。

著者は英語が全くできません

ミスがあればどんどん訂正コメントして頂ければm(__)m


Async Actions

basicガイドではシンプルなtodoアプリケーションを作りました。

これは同期的でした。

Basicではアクションや、ステートはすぐに更新されます。

このガイドでは非同期的なアプリケーションを作ります。

Redditのapiを使い現在のヘッドラインを表示するアプリケーションです。

どのようにredux flowを非同期的にさせればいいでしょう?


Actions

もし非同期にapiを呼び出したいとき、2つの重要な場面があります、コールをしたときと、レスポンスを受け取るときです(もしくはタイムアウトしたとき)

その2つの瞬間は普通アプリケーションのstateの変更をします。

それを行うには、同期的なreducersによって処理されたactionsをdispatchする必要があります。

・ An action informing the reducers that the request began

reducersがisFetchingをトグルすることによってこのアクションを処理できます。

これでスピナーを見せるタイミングを把握します。

・ An action informing the reducers that the request finished successfully

reducersが新しいデータをステートにマージし、isFetchingをリセットすることによってこのactionを処理できます。

スピナーを隠し、fetchされたデータを表示します。

・ An action informing the reducers that the request failed

ReducersはisFetchingをリセットすることによってこのアクションを処理できます。

加えて、いくつかのreducersはエラーメッセージを保存することもできます。

actionsには専用のステータスフィールドを使用することができます。


status.js

{ type: 'FETCH_POSTS' }

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

または、下記のように定義できます。(separate type)


status.js

{ type: 'FETCH_POSTS_REQUEST' }

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

Single action typeを使用するか、multiple action typeを使用するかどうか選択してください。

ここの規約(どちらを使用するか)はチームで決定する必要があります。

Multiple types 間違いの余地を少し残します。しかし、これは問題ではありません。

もしaction createrとreducer作るならredux-actionsのような補助的なライブラリーがあるからです。このチュートリアルではseparate typesをつかいます。

Synchronous Action Creators

まずは同期的な action types と action creater 作っていきましょう。

これによってユーザーはsubredditを選択し表示することができるようになります。


actions.js

export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'

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


refreshボタンを押すとupdateされます。


actions.js

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'

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


これらのactionsはユーザーの操作を制御します。

しかしまだ違う種類のactionを作る必要があります、ネットワークリクエストを処理するactionsです。これは後で実装することにしますが、とりあえず定義だけしておきます。

Subredditをフェッチするとき、REQUEST_POSTSをdispatchします。


actions.js

export const REQUEST_POSTS = 'REQUEST_POSTS'

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


上のコードはSELECT_SUBREDDIT、INVALIDATE_SUBREDDITから切り離すために重要です

アプリが複雑になるにつれて独立したアクションからデータをフェッチする場合があります。(例えば、最も人気のあるsubbredditを取ってきたり、古いデータを更新するとき)

また、ルート変更に応じてフェッチすることもあるかもしれません。

なので早い段階で2つのアクションともフェッチするように実装することはあまり賢くありません

最後に、ネットワークリクエストが来た時、RECEIVE_POSTSをdispatchします。


actions.js

export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'

export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'

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

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

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

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


これが今、私達がしっておくべき全てです

ネットワークリクエスト部分などはまた後に議論します。


Designing the State Shape

basic tutorial では実装に入る前にstateの設計をしておく必要があります。

非同期なコードでは、更に多くの扱うstateがあり、

それを考える必要があります。

このパートは初心者にとって難しいかもしれません。

どのような情報を非同期アプリケーションのstateとして記述すべきか、単一のツリーでどう整理すべきか、すぐに分からないからです。

もっとも一般的な使用例から始めましょう、listsです。

web application はよくリストが実装されています。

例えば、ポストのリスト、フレンドのリストなどです。

どのようなリストを表示するか考える必要があります。

必要な場合にのみキャッシュし、取り出すことができるので、stateにそれらを別々に保存したいとします。

reddit heddlines app のstate設計はこのようになっています

selectedSubreddit: 'frontend',

postsBySubreddit: {
frontend: {
isFetching: true,
didInvalidate: false,
items: []
},
reactjs: {
isFetching: false,
didInvalidate: false,
lastUpdate: 1439478405547,
item: [
{
id: 42,
title: 'Confusion about Flux and Relay'
},
{
id: 500,
title: 'Createing a simple Application Using React JS'
}
]
}
}


Handling Actions

ネットワークリクエストと一緒にdispatchの実装に入る前に、actionに対するreducerを書いていきます。


reducer.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.assing({}, state, {
[action.subreddit]: posts(state[actions.subreddit], action)
})
default:
return state
}
}

const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})

export default rootReducer


このコードには2つの興味深い部分があります。

・ ES6プロパティの構文を使用して更新できます。

return Object.assing({}, state, {

[action.subreddit]: posts(state[actions.subreddit], action)
})

これは下記と同じです

let nextState = {}

nextState[action.subreddit] = posts(state[action.subreddit], action)
return Object.assign({}, state, nextState)

・ post(state, action)は特定のポストのリストのstateを管理しています。

これは単なるreducer compositionなのです!

これがどのようにreducerを分割するかの方法です、そしてこの方法では、アイテムのオブジェクトの更新をposts reducerに委任しています


Async Action Creators

最後に、私達はどのようにネットワークと同期的action creatorを使うといいでしょうか?

標準的な方法ではredux-thunk middlewareを使います。

middlewareの挙動に関してはまた後で説明することにします。

今、あなたが知るべき一つの重要なことがあります。

action creatorはactionオブジェクトの代わりに

functionを返すことができます。

この方法ではaction creatorはthunkになります。

action creatorがfunctionを返すとき,functionはreduxのthunk middlewareによって実行されます。

この関数はピュアでなくてもいいのです

非同期api呼び出しを実行するなどの副作用を持つことが許可されているのです。

そしてこの関数はactionをdispatchできます。

私達はこのように特別なthunk action creatorを定義することができます。


actions.js

import fetch from 'isomorphic-fetch'

export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'

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

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

export function fetchPosts(subreddit) {
return (dispatch) {
dispatch(requestPost(subreddit))

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

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

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


Redux thunk middlewareをどのようにdispatchのメカニズムに含めばいいでしょうか?

下記のようにreduxのstore エンハンサーであるapplyMiddlewareを使います。


index.js

import React from 'react'

import { render } from 'react-dom'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'

const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
)

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

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


これで徐々により洗練された非同期制御フローを記述できます。

thunk middleware は非同期actionを実装するための唯一の方法ではありません。


次のステップ

参考 http://redux.js.org/docs/advanced/AsyncActions.html

redux-thunk https://github.com/gaearon/redux-thunk