LoginSignup
28
14

More than 5 years have passed since last update.

redux-sagaを用いた非同期通信

Last updated at Posted at 2018-12-17

グレンジ Advent Calendar 2018」18日目を担当するy244です。
株式会社グレンジにてフロントエンジニアをしております。

概要

redux-sagaによる非同期通信のQiitaの記事はあるが断片的なものが多く、いざ設計となると悩む事が多かったのでまとまった形でのサンプルコードを用意してみました。
非同期通信の処理をどこでもつかというところが主な内容となっています。
【今回のサンプルの仕様】
ログインフォームからユーザーの入力内容(email,password)を任意のAPIに投げてサーバーからのレスポンスで処理を行う

対象

react, redux, redux-sagaの仕組みはある程度わかったけど運用前提の設計をどうしようか悩んでいる方

ディレクトリ構成

public
 └index.html
src
 ├actions.js
 ├common
 │   └api.js 
 ├components
 │   └login-form.js
 ├reducers
 │   ├index.js
 │   └login.js
 ├sagas
 │   ├index.js
 │   └loginSaga.js
 ├containers
 │   └login.js
 ├index.js
 └router.js
 package.json
index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>sample login form</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
  </body>
</html>

index,router

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router';

ReactDOM.render(<Router />, document.getElementById('root'));
router.js
import React from 'react';
import { Provider } from 'react-redux';
import { HashRouter, Route, Switch } from 'react-router-dom';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import { routerMiddleware } from 'react-router-redux';
import routerReducer from './reducers/index';
import rootSaga from './sagas/index';

import Login from './containers/login';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  routerReducer,
  composeEnhancers(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);

// ステートが変更されるたびにログ出力
store.subscribe(() =>
  console.log("store", store.getState());
);

function Router() {
  return (
    <Provider store={store}>
      <HashRouter>
        <React.Fragment>
          <Switch>
            <Route path='/login' component={Login} />
          </Switch>
        </React.Fragment>
      </HashRouter>
    </Provider>
  );
}
export default Router;

Action

redux-actionsを用いて記述を簡略化しActionCreatorを生成しています。

actions.js
import { createAction } from 'redux-actions';

export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
export const loginRequest = createAction(LOGIN_REQUEST);
export const loginSuccess = createAction(LOGIN_SUCCESS);
export const loginFailure = createAction(LOGIN_FAILURE);

Reducer

reducers/index.jsのcombineReducersにてReducerをまとめています。
(今回はReducerは一つですが複数の分割ファイルになるのを想定しての設計になっています)

index.js
import { combineReducers } from 'redux';
import login from './login';

// Reducer Index
const rootReducer = combineReducers({
  login,
});

export default rootReducer;
login.js
const initialState = {};
export default function login(state = initialState, action) {
  switch (action.type) {
    case 'LOGIN_SUCCESS':
      // ログイン成功時に呼ばれる
      state = action.payload;
      return state;
    case 'LOGIN_FAILURE':
      // ログイン失敗時に呼ばれる
      state = action.payload;
      return state;
    default:
      return initialState;
  }
}

Saga

sagas/index.jsにて複数のsagaファイルをまとめています。

index.js
import { all } from 'redux-saga/effects';
import loginSaga from './loginSaga';

export default function* rootSaga() {
  yield all([
    ...loginSaga,
  ]);
}
loginSaga.js
import { call, put, takeEvery } from 'redux-saga/effects';
import api from '../common/api';
import { loginSuccess, loginFailure } from '../actions/actions';

function* handleRequest(action) {
  let { payload } = action;
  payload = yield call(api, payload);

  // サーバーからのレスポンスデータによる分岐処理
  if( ログイン成功か失敗かの条件 ) {
    // 成功時アクション呼び出し
    yield put(loginSuccess(payload));
  } else {
    // 失敗時アクション呼び出し
    yield put(loginFailure(payload));
  }
}

// 「LOGIN_REQUEST」アクションが呼ばれるのを待つ呼ばれたhandleRequestを実行
const loginSaga = [takeEvery('LOGIN_REQUEST', handleRequest)];
export default loginSaga;

Components

Presentational component見た目だけを扱うComponent

login-form.js
import React, { Component } from 'react';

class LoginForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      loginForm: {
        email: "",
        password: ""
      }
    }

    this.handleChange = this.handleChange.bind(this);
  }

  // ログイン入力内容の変更を監視
  handleChange(event) {
    let loginForm = this.state.loginForm;
    switch (event.target.name) {
      case 'login-email':
        loginForm.email = event.target.value;
        break;
      case 'login-password':
        loginForm.password = event.target.value;
        break;
      default:
        break;
    }

      // 状態を更新
      this.setState({
        loginForm: loginForm
      });
  }

  componentDidMount(){
    let loginForm = this.state.loginForm;
    this.setState({
      loginForm: loginForm
    });
  }

  render() {
    const { dispatchRequest } = this.props;
    return (
      <form>
        <div>
          <div>
            <input type="email" name="login-email" placeholder="メールアドレス" value={this.state.loginForm.email} onChange={this.handleChange} />
            <input type="password" name="login-password" placeholder="パスワード" value={this.state.loginForm.password} onChange={this.handleChange} />
          </div>
          <div>
            <button type="button" disabled={false} onClick={e => dispatchRequest(this.state.loginForm)}>ログイン</button>
          </div>
        </div>
      </form>
    );
  }
}

Container

Container component componentのロジカルな部分を担う
具体的なデータの取り扱いstoreからのコールバックを
Presentational componentへ受け渡す

login.js
import React, { Component } from 'react';
import SectionTitle from '../components/section-title';
import { connect } from 'react-redux';
import LoginForm from '../components/login-form';
import { loginRequest } from '../actions';


class Login extends React.Component {
  render() {
    return (
      <section>
        <SectionTitle title="ログイン" />
        <LoginForm dispatchRequest={this.props.dispatchRequest} />
      </section>
    );
  }
}

function mapStateToProps(state) {
  return state.login;
}
function mapDispatchToProps(dispatch) {
  return { dispatchRequest: loginForm => dispatch(loginRequest(loginForm)) }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login);

API

非同期通信部分はFetch APIを使用

api.js
function api(data) {
  return fetch('https://xxxxxxxxxx', {
    method:'post',
    mode: 'cors',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json; charset=utf-8'
    },
    body: JSON.stringify(data)
  })
    .then(res => (res.json()))
    .catch(error => ({ error }));
}

export default api;

package.json

package.json
{
  "name": "sample",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.6.0",
    "react-dom": "^16.6.0",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.0.5",
    "redux-actions": "^2.6.4",
    "redux-saga": "^0.16.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
  },
  "devDependencies": {
    "@babel/core": "^7.1.2",
    "babel-loader": "^8.0.4",
    "fetch": "^1.1.0",
    "node-sass": "^4.9.4",
    "react-redux": "^6.0.0"
  }
}

まとめ

サーバー側が用意するAPI毎にactions.jsへのactionの追加とreducer,sagaファイルを追加していく形になっています。
今回、Actionは1ファイルにまとめ、Reducerは分割で持つ形にしていますが
Action,Reducerは対のものなのでAction,Reducerで一つのファイルにまとめ用途ごとに分割の形でも良いと思いました。

28
14
0

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
28
14