LoginSignup
246
244

More than 3 years have passed since last update.

React + ReduxでREST APIを叩いてリスト表示する方法

Last updated at Posted at 2018-01-08

1. はじめに

React + Reduxで,ajaxでデータを取得してリストを表示しようとしたら,なかなかにハマったのでメモががてら共有したいと思います。(ひととおりドキュメントを読んでから実装したにも関わらず,ハマった。。。)

1-1. アウトライン

本記事のアウトラインは以下です。

  1. はじめに
    1-1. アウトライン
    1-2. 作りたい機能概要
  2. Middlewareの導入
  3. Actionの実装
  4. Reducerの実装
  5. Componentsの実装
    5-1. Container Component
    5-2. Presentational Component
  6. まとめ

1-2. 作りたい機能概要

作るのは,ユーザの投稿(Post)の一覧を表示する機能です。サーバー側はGETリクエストすると

{
    _id: 'aaaaaaaa',
    body: 'bbbbbbbb',
    created_at: 'cccccccc',
    updated_at: 'dddddddd'
}

というjsonを返す仕様になっています。このAPIにアクセスしてデータを取得し,画面に表示するという機能です。
表示は次のような感じで,bodyのテキストを表示します。

スクリーンショット 2018-01-08 11.16.22.png

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.jsxcreateStore()にapplyMiddleware()を設定することでできます。

index.jsx
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を使用します。

コードは以下のようになっています。

actions/Action.jsx
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は以下のようになります。

postReducer.jsx
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()で読み込んでいます。

rootReducer.jsx
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は以下のようになります。

containers/GetPostListContainer.jsx
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は以下のようになります。

components/PostList.jsx
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コンポーネントにデータを渡せないという現象に頭を悩まされました。うまくいったコードは以下です。

components/Post.jsx
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をセットします。

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

246
244
6

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
246
244