How to build Rails5 API + Redux ToDo Application その4

前回はクライアントをRedux化(API叩かない版)しました。

kogoto.hatenablog.com

これにAPIを叩かせて完成とします。

github.com

CORS

実装前に、クロスドメインの問題があるので、Railsのほうでごにょごにょします。

# config/application.rb
module Rails5ReactReduxTodo
  class Application < Rails::Application

    # ...

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :patch, :delete]
      end
    end
  end
end

SuperAgent

AJAXしたいだけなのでjQueryではなくHTTPリクエストに特化したライブラリを利用します。

github.com

$ npm install --save superagent

まず、Railsサーバーを起動してREPLからAPIを叩いてみます。

サーバーを起動

$ rails s

REPLから実行

$ node

get: ToDO一覧を取得

> request = require('superagent')
> request.get('http://localhost:3000/todos').end(function(err, res) { console.log(res.body) })
> [ { id: 1,
    title: '単2電池を買いに行く',
    completed: false,
    order: 1,
    created_at: '2016-07-29T00:20:23.797Z',
    updated_at: '2016-07-29T00:20:23.797Z' } ]

post: ToDOを登録

> request.post('http://localhost:3000/todos').send({ todo: { title: 'ストレッチ', order: 1 }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  title: 'ストレッチ',
  completed: null,
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:23:17.983Z' }

patch: ToDOを完了状態に更新

> request.patch('http://localhost:3000/todos/2').send({ todo: { completed: true }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  completed: true,
  title: 'ストレッチ',
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:25:39.456Z' }

delete: ToDOを削除

> request.del('http://localhost:3000/todos/2').end(function(err, res) { console.log(res.body) })
> {}

こんな感じでWeb APIにアクセスできます。

redux-promise

redux-promiseはcreateActionにPromiseを渡すとよしなにしてくれるMiddlewareです。
redux-actionsと併せて使います。

まずはPromiseを返すWeb APIを作成します。
以下はToDO一覧を取得するAPIの場合です。

// api/todos.js
export default {
  fetchTodos: () => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .end((err, res) => {
          if (!res || res.status === 404) {
            reject()
          } else {
            resolve(res.body)
          }
        })
    })
  },
  /* ... */
}

Action作成時にこの関数を渡します。

// actions/api.js
export const fetchTodos = createAction(ApiAction.FETCH_TODOS, api.fetchTodos)

Actionのハンドリングをします。
nextのaction.payloadにはresolveの引数であるbody(todos)が格納されています。

// reducers/todos.js
const todos = handleActions({
  [ApiAction.FETCH_TODOS]: {
    next: (state, action) => action.payload,
    throw: (state, aciton) => []
  },
  /* ... */
}, [])

このActionをcomponentDidMount時に呼び出します。

// App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actionCreators from './actions/api'

export default class App extends Component {
  componentDidMount() {
    this.handleFetchTodos()
  }

  handleFetchTodos() {
    this.props.fetchTodos()
  }
  /* ... */
}

App = connect(null, actionCreators)(App)

export default App

これを実行しredux-loggerを利用してActionのログを見てみます。

f:id:ktdk:20160730125821p:plain

自前でログを仕込んでみたところrequest, responseの間に1回目のFETCH_TODOSが発行されています。
(おそらくこの部分はredux-promiseが処理してくれている)

で、resolveのタイミングで2回目のFETCH_TODOSが発行されhandleActionsのnextがコールされます。

これで非同期でAPIを叩いて動くToDOアプリケーションの完成です。

つぎは何作ろっかなー