1. はじめに
React + Reduxで,ajaxでデータを取得してリストを表示しようとしたら,なかなかにハマったのでメモががてら共有したいと思います。(ひととおりドキュメントを読んでから実装したにも関わらず,ハマった。。。)
1-1. アウトライン
本記事のアウトラインは以下です。
- はじめに
1-1. アウトライン
1-2. 作りたい機能概要 - Middlewareの導入
- Actionの実装
- Reducerの実装
- Componentsの実装
5-1. Container Component
5-2. Presentational Component - まとめ
1-2. 作りたい機能概要
作るのは,ユーザの投稿(Post)の一覧を表示する機能です。サーバー側はGETリクエストすると
{
_id: 'aaaaaaaa',
body: 'bbbbbbbb',
created_at: 'cccccccc',
updated_at: 'dddddddd'
}
というjsonを返す仕様になっています。このAPIにアクセスしてデータを取得し,画面に表示するという機能です。
表示は次のような感じで,body
のテキストを表示します。
2. Middlewareの導入
まず,Reduxで非同期通信をするためには, redux-thunkというミドルウェアを使用する必要があります。Reduxアプリケーションにおいて,Action CreatorはシンプルなActionオブジェクトを返すことしかできない(非同期APIの呼び出しとかしてはいけない)のですが,Thunkを使うとそれが可能になります。Action Creatorで関数を返すと,Thunkがそれを受け取ってうまいこと処理してくれるようです。
redux-thunkはnpmでインストールできます。
$ npm i --save redux-thunk
ついでに,redux-loggerも導入しておくと,状態の変更毎にログを出してくれるのでデバック時に役立ちます。これもnpmでインストールできます。
ミドルウェアの導入は, index.jsx
でcreateStore()
にapplyMiddleware()を設定することでできます。
import React from 'react'
import { render } from 'react-dom'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import App from '~/components/App'
import rootReducer from '~/reducers/rootReducer'
import { getPosts } from '~/actions/postAction'
const store = createStore(
rootReducer,
applyMiddleware(thunk, logger)
)
store.dispatch(getPosts())
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
なお,今回はこのファイルの読み込み時に,後でActionで定義するgetPosts()をdispatchしています。(今はとりあえず表示できればいいので)
3. Actionの実装
次にActionを実装します。Reduxの初学者が非同期通信を使用するアプリを実装する際,
「どこでリクエストを投げるのが適切なのか」
という問題が立ちはだかると思います。
答えは,**「ミドルウェアを導入して,Action Creatorで行なう」**です。最初,Reducerで処理を記述したほうが自然な気がしてしまいます(僕だけ?)が,それは気のせいです。
Actionで必要なデータを準備してdispatchし(ミドルウェアを利用),Reducerでactionが持つ情報をもとに,stateを更新するというのが正しい方法のようです。Actionは基本的にシンプルなオブジェクトを返さなければいけないため,本来であれば非同期APIをコールするというような複雑な処理はできません。そこをミドルウェアを使用するとできるようになるようです。
公式ドキュメントに,Action Creatorを拡張する様々な方法が書いてあります。その中でも,一番良く使われるのがThunkミドルウェアを使用する方法のようなので,今回はThunkを使用します。
コードは以下のようになっています。
import axios from 'axios'
export const GET_POSTS_REQUEST = 'GET_POSTS_REQUEST'
const getPostsRequest = () => {
return {
type: GET_POSTS_REQUEST
}
}
export const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'
const getPostsSuccess = (json) => {
return {
type: GET_POSTS_SUCCESS,
posts: json,
receivedAt: Date.now()
}
}
export const GET_POSTS_FAILURE = 'GET_POSTS_FAILURE'
const getPostsFailure = (error) => {
type: GET_POSTS_FAILURE,
error
}
export const getPosts = () => {
return (dispatch) => {
dispatch(getPostsRequest())
return axios.get(`http://localhost:3000/api/v1/posts`)
.then(res =>
dispatch(getPostsSuccess(res.data))
).catch(err =>
dispatch(getPostsFailure(err))
)
}
}
GETするときに必要になるActionは全部で3つです。
-
GET_POSTS_REQUEST
:リクエスト開始時にdispatchされ,isFetchingをtrueにする -
GET_POSTS_SUCCESS
:リクエスト成功時にdispatchされ,isFetchingをfalseにしてデータをstateにセットする -
GET_POSTS_FAILURE
:リクエスト失敗時にdispatchされ,isFetchingをfalseにしてエラーオブジェクトをstateにセットする
getPosts()
をdispatchすることでリクエストを送信してデータをストアすることができます。
また,公式ドキュメントではisomorphic-fetch
を使用していますが,今回はaxios
ライブラリを使用しています。
4. Reducerの実装
Reducerは以下のようになります。
import {
GET_POSTS_REQUEST, GET_POSTS_SUCCESS, GET_POSTS_FAILURE
} from '~/actions/postAction'
const initalState = {
isFetching: false,
items: []
}
const posts = (state = [initalState], action) => {
switch (action.type) {
case GET_POSTS_REQUEST:
return [
...state,
{
isFetching: true,
items: []
}
]
case GET_POSTS_SUCCESS:
return [
...state,
{
isFetching: false,
items: action.posts,
lastUpdated: action.receivedAt
}
]
case GET_POSTS_FAILURE:
return [
...state,
{
isFetching: false,
error: action.error
}
]
default:
return state
}
}
export default posts
GET_POSTS_SUCCESS
アクションがdispatchされると,actionからpostsを取り出してitemsに格納しています。このデータをコンポーネントで使用することになります。尚,stateは基本的に上書きせずに,配列の後ろに追加していくことに注意してください。
また上のReducerはcombineReducer()
で読み込んでいます。
import { combineReducers } from 'redux'
import posts from './postReducer'
const rootReducer = combineReducers({
posts
})
export default rootReducer
5. Componentの実装
ここが一番ハマりました。今思えば,Reduxというよりjsvascriptの基本的な事項の理解が足りてなかった。。。
さて,Reduxアプリケーションでは,コンポーネントが2種類必要になります。この2つはざっくり以下のような特徴があります。
- Container Component
=> Reduxのstoreに格納されているデータをPresentational Componentに紐付けるためのコンポーネント。 - Presentational Component
=> Reactアプリの普通のコンポーネント。Container Componentで紐付けられたデータを使用して実際に画面を描画する。
それぞれについて見ていきます。
5-1. Container Component
Container Componentでは,Presentational Componentで必要になるデータをstateから取り出し,
connect('渡したいオブジェクト')('コンポーネント')
としてComponentへ紐付けます。
リストを取得するContainer Componentは以下のようになります。
import {connect} from 'react-redux'
import PostList from '~/components/post/PostList'
const mapStateToProps = (state) => {
const length = state.posts.length
const currentState = state.posts[length - 1] // 一番新しいstateを取り出す
return { posts: currentState.items } // 描画するのに必要なのはとりあえずitemsだけなのでitemsだけ返す
}
const GetPostList = connect(
mapStateToProps
)(PostList)
export default GetPostList
5-2. Presentational Component
Presentational Componentは以下のようになります。
import React from 'react'
import PropTypes from 'prop-types'
import Post from './Post'
const PostList = ({ posts }) => (
<ul>
{ posts.map((post, index) =>
<Post key={index} {...post}/>
)
}
</ul>
)
PostList.propTypes = {
posts: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.object.isRequired,
body: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired,
updated_at: PropTypes.string.isRequired
}).isRequired
).isRequired
}
export default PostList
prop-types
を使って,posts
の型を定義しています。Container Componentで渡したデータの形を定義しておきます。
渡したデータが間違っていた場合,エラーを出してくれます。
ここで,上手くPostコンポーネントにデータを渡せないという現象に頭を悩まされました。うまくいったコードは以下です。
import React from 'react'
import PropTypes from 'prop-types'
const Post = ({ _id, body }) => (
<li>
{ body }
</li>
)
Post.propTypes = {
post: PropTypes.shape({
_id: PropTypes.object.isRequired,
body: PropTypes.string.isRequired,
created_at: PropTypes.string.isRequired,
updated_at: PropTypes.string.isRequired
})
}
export default Post
これを,ずっと
const Post = ({ post }) => (
<li>
{ post.body }
</li>
)
としていて上手く渡せていませんでした。これ,よく考えるとpostの要素を直接引数に設定して,受け取らないとだめですよね。
なぜなら,PostList.jsx
でPostコンポーネントにデータを渡す際に,
<Post key={index} {...post}/>
として, post
オブジェクトを展開しています。よって,渡されるデータは展開されたあとのデータになるので,要素名で受け取らないと受け取れないということです。ヤラレタ。。。というか,これRedux,Reactというより,ES6の理解不足ですね。
最後に,App.jsxは以下のようにして,Container Componentをセットします。
import React, {Component} from 'react'
import GetPostList from '~/containers/GetPostListContainer'
const App = () => (
<div>
<GetPostList />
</div>
)
export default App
補足
直接Reduxとは関係ないですが,ちょっとハマったのでメモしておきます。
map関数のコールバックの中身の括弧の違いで,子コンポーネントがreturnされるかどうか違うようです。
おそらく当たり前のことなんだろうけど,最初わからなくてなんで描画されないんだ〜!ってなりました。試していたら以下のような法則があるっぽい。
posts.map(post => {
<Post />
})
↑ Postコンポーネントはreturnされないため表示されない
posts.map(post =>
<Post />
)
posts.map(post => (
<Post />
))
↑ Postコンポーネントがreturnされ,リストが表示される
6. まとめ
ES6のスプレッド演算子とか,コールバック関数のreturnの仕様とかではまりました。javascriptの基本なのに理解できておらず,お恥ずかしい。。。
でも,悩む過程でReact + Reduxの理解が深まったので良しとしましょう。誰かのお役に立てれば幸いです。
参考
Redux公式ドキュメント
Reduxサンプル
reduxを試してみた(5日目) - ajaxを使ってUIを構築する(reduxにおける非同期の制御)
Fetch vs. Axios.js for making http requests
Reduxの基本 ~ 公式ドキュメント Basics~
ReduxのAdvancedな使い方 ~ 公式ドキュメント Advanced~