読者です 読者をやめる 読者になる 読者になる

あいつの日誌β

あいつの日誌です。

React tutorial (4)

React.js Redux Tutorial

あらすじ

前回は redux による state の管理について触れました。 今回は http 通信などによる非同期処理の方法について触れます。

なお外部との通信に https://jsonplaceholder.typicode.com を利用しています。

目次

  • 開発環境を準備
  • React の基本的な Life Cycle に触れる
  • redux に触れる
  • redux-saga に触れる <= 今日やること
  • react router に触れる
  • npm で公開されている components を導入して echo system を体感する
  • redux-form に触れる
  • react-select に触れる

準備

npm install --save redux-saga axios
npm install --save-dev http-proxy-middleware
touch src/api.js src/sagas.js

create src/api.js

import axios from 'axios'

const BASE_URL = window.location.origin

export function getPosts({userId}) {
  return axios.get(`${BASE_URL}/api/posts?userId=${userId}`).then((res) => {
    return { "posts": res.data }
  })  
}

export default { getPosts }

edit src/actions.js

import { createAction } from "redux-actions"
import api from './api'

export const FETCH_REQUESTED = "FETCH_REQUESTED"
export const FETCH_SUCCESSED = "FETCH_SUCCESSED"
export const FETCH_FAILED    = "FETCH_FAILED"

export const fetchRequested = createAction(FETCH_REQUESTED)
export const fetchSuccessed = createAction(FETCH_SUCCESSED)
export const fetchFailed    = createAction(FETCH_FAILED)

export function getPosts({userId}) {
  return fetchRequested(() => {
    return api.getPosts({userId})
  })
}

edit src/reducers/index.js

import { combineReducers } from 'redux'
import { FETCH_SUCCESSED } from '../actions'

const initialState = { 
  posts: []
}

function fetch(state = initialState, action) {
  switch (action.type) {
  case FETCH_SUCCESSED:
    return Object.assign({}, state, action.payload)
  default:
    return state
  }
}

const rootReducer = combineReducers({
  fetch,
})

export default rootReducer

edit src/store/configureStore.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import createLogger from 'redux-logger'

import rootReducers from '../reducers'
import rootSaga from "../sagas"

const sagaMiddleware = createSagaMiddleware()
const logger = createLogger()

const middlewares = [sagaMiddleware, logger]
const createStoreWithMiddleware = applyMiddleware(...middlewares)(createStore)

export default function configureStore(initialState) {
  const store = createStoreWithMiddleware(rootReducers, initialState)
  if (module.hot) {
    module.hot.accept('../reducers', () => {
      const nextRootReducer = require('../reducers').default
      store.replaceReducer(nextRootReducer)
    })
  }
  sagaMiddleware.run(rootSaga)
  return store
}

create src/sagas

import { takeEvery } from "redux-saga"
import { put, call } from "redux-saga/effects"
import api from "./api"

import {
  FETCH_REQUESTED,
  fetchSuccessed,
  fetchFailed,
} from "./actions"

function* fetch(action) {
  try {
    const data = yield call(action.payload)
    yield put(fetchSuccessed(data))
  } catch (err) {
    yield put(fetchFailed(err))
  }
}

export default function* rootSaga() {
  yield takeEvery(FETCH_REQUESTED, fetch)
}

edit src/containers/Posts.js

import React, {Component} from 'react'
import { connect } from 'react-redux'
import { fetchRequest } from '../actions'
import { PostItem } from '../components/Posts'
import { getPosts } from '../actions'

class Posts extends Component {

  handleButtonClick() {
    this.props.dispatch(getPosts({userId: 1}))
  }

  render() {
    const {posts} = this.props.fetch

    return (
      <div className="container">
        <h2>Posts</h2>
        {posts.map((post) => (
          <PostItem key={post.id} post={post} />
        ))}
        <button className="btn btn-default" onClick={this.handleButtonClick.bind(this)}>Push</button>
      </div>
    )
  }
}

const select = state => (state)

export default connect(select)(Posts)

edit devServer.js

const webpack = require('webpack')
const browserSync = require('browser-sync')
const proxy = require('http-proxy-middleware')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const historyApiFallback = require('connect-history-api-fallback')

const config = require('./webpack.config')
const bundler = webpack(config)

const apiProxy = proxy('/api', {
  pathRewrite: {'^/api' : ''},
  target: 'https://jsonplaceholder.typicode.com',
  changeOrigin: true,
  logLevel: 'debug'
})

browserSync({
  open: false,
  server: {
    baseDir: config.output.path,
    index: "index.html",
    middleware: [
      apiProxy,
      webpackDevMiddleware(bundler, {
        publicPath: config.output.publicPath,
        stats: { colors: true }
      }),
      webpackHotMiddleware(bundler),
      historyApiFallback()
    ]
  },
  files: [
    'www/*.html'
  ]
})

Implement it and click button.

% node devServer.js

説明

非同期での処理の場合はイベントの発生を FETCH_REQUESTED, FETCH_SUCCESSED, FETCH_FAILED とします。 こうすることで WEB API への通信が開始してから完了するまで loading bar を表示したり、通信の成否に応じて Notification を表示することができます。

まとめ

今回は redux による非同期処理について触れました。 次回は react router について触れます。