Help us understand the problem. What is going on with this article?

ReactでAPI処理はredux-sagaを使うのがオススメ!

More than 1 year has passed since last update.

ReactでAPI処理をする必要がある時って色々複雑だとおもいます。
僕も初めはググりまくってやっていきましたが、MiddleWareを作れredux-thunkを使えだの書いてあり、とりあえずそれらに従い一通りつかってみました。そのあとにredux-sagaの存在を知り、深く調べていくと、redux-sagaの方が優れているんじゃないかと個人的におもい、redux-sagaを使用してみました。

すると、reduxの処理がしっかり分別でき、reduxの理解がより一層深まり習得による効果は大きかったです。
ですので、redux-sagaを使用すればreduxをより一層理解した上で構築できます。

  • 「react + reduxでAPI処理したいけど書き方わからん」
  • 「そもそもreduxってどうやって書けばいいの?」

こういった悩みも解消できるよう、react-sagaを使用したAPI処理の方法を説明していきます。

redux-sagaをインストールしよう

前提としてReact + Reduxの開発環境が整っていることとします。

npm install --save-dev redux-saga または
yarn add -D redux-saga でredux-sagaをインストールしましょう。

ちなみにredux-sagaとは下記のことだそうです。

redux-saga は React/Redux アプリケーションにおける副作用(データ通信などの非同期処理、ブラウザキャッシュへのアクセスのようなピュアではない処理)をより簡単で優れたものにするためのライブラリです。

Saga はアプリケーションの中で副作用を個別に実行する独立したスレッドのような動作イメージです。 redux-saga は Redux ミドルウェアとして実装されているため、スレッドはメインアプリケーションからのアクションに応じて起動、一時停止、中断が可能で、Redux アプリケーションのステート全体にアクセスでき、Redux アクションをディスパッチすることもできます。

ES6 の Generator 関数を使うことで読み書きしやすく、テストも容易な非同期フローを実現しています。それにより非同期フローが普通の同期的な JavaScript のコードのように見えます(async/await と似ていますが Generator 関数にしかないすごい機能があるんです)。

これまで redux-thunk を使ってデータ通信を行っているかもしれませんが、 redux-thunk とは異なりコールバック地獄に陥ることなく、非同期フローを簡単にテスト可能にし、アクションをピュアに保ちます。

引用元: GitHub https://github.com/redux-saga/redux-saga/blob/master/README_ja.md

正直これを読んでパッと理解できているかというと曖昧ですが、要約すると
API処理を簡単に書けてredux-thunkよりも扱いやすい! って書かれています。僕はそう解釈しました。

それでは早速redux-sagaをつかっていきます!

必要なファイルを作成していこう!

今回はサンプルのコードを作成していきます。APIは国情報が取得できるAPIを使用してみます。すごい情報量いっぱいのAPIです。それを使用して国名をreducerに蓄積してみましょう。
引用元:REST Countries https://restcountries.eu/

まずはactionを作ります。

country/action.js
export const FETCH_COUNTRY = 'FETCH_COUNTRY'
export const SUCCESS_COUNTRY_API = 'SUCCESS_COUNTRY_API'
export const FAIL_COUNTRY_API = 'FAIL_COUNTRY_API'

export const fetchCountry = () => {
  return {
    type: FETCH_COUNTRY,
    items: []
  }
}

export const successCountryApi = (response) => {
  return {
    type: SUCCESS_COUNTRY_API,
    items: response
  }
}

export const failCountryApi = (error) => {
  return {
    type: FAIL_COUNTRY_API,
    items: error
  }
}

3つのアクションを用意します。それぞれの用途は下記に書いてある通りです。

action名 用途
FETCH_COUNTRY APIリクエストをするアクション
SUCCESS_COUNTRY_API API接続成功した時のアクション
FAIL_COUNTRY_API API接続失敗した時のアクション

コード後半部分にはアクションが実行された時の処理内容が書かれています。基本的にアクションの返り値はアクションのtype = アクション名とstateに保存したいデータを入れたオブジェクトを返しています。引数にresponseerrorと置いているので、それぞれのアクションを実行する時に、渡したいデータを渡してあげると良いです。今回はresponseに国名だけ入った配列データを渡して、itemsに入れてあげる処理をしたいとおもいます。

アクションは準備ができました!
続いてMiddleWareの処理を書くためにAPI処理と今回の本題であるredux-sagaを書いていきましょう!

API処理の関数を作成する

最初にAPIを実行する関数を作ってあげましょう。API実行関数を別ファイル化する場合もありますが、今回はsaga.jsに書いていきます。

country/saga.js
import axios from 'axios'

const requestCountryApi = () => {
  const url = 'https://restcountries.eu/rest/v2/all'
  return axios
    .get(url)
    .then((response) => {
      const country = response.data.map((item) => item.name)
      return { country }
    })
    .catch((error) => {
      return { error }
    })
}

API通信はaxiosを使用していますので yarn または npmで axios をインストールしましょう。
インストールしたらimport axios from 'axios'でimportしてあげましょう。

そしてAPI通信です。国情報を取得できるAPIのURLは https://restcountries.eu/rest/v2/all です。このURLでgetメソッド通信します。

返却値は色々あるのですが、国名だけ取得したいのでmap関数を使用して国名だけ取得したものをreturnで返しています。API通信の内容はざっくりとこんな感じでいきます。

それではactionとジェネレーター関数を使用してredux-sagaの処理内容を書いていきます。

ジェネレータ関数を作成する

redux-sagaにはいくつかのコマンドが用意されており、それらを使用することにより非同期処理の実行順序をコントロールすることができます。今回使用していくコマンドがどのように使われていくかは簡単に下記に記載しておきます。他にも様々なコマンドがあるので興味のある方はこちらから参照してみてください。

コマンド 概要
call 非同期処理の完了を待つ
put アクションを実行する
takeLatest 実行中の処理を中断し、新しく処理をおこなう
all effectを並列処理で実行する

それでは処理内容を詳しくみていきます。

まず、ジェネレータ関数を作成します。実行内容はAPIを叩いて成功と失敗時で実行内容を分けてあげるような内容です。とりあえずfetchCountryという名前で作成しましょう。

country/saga.js
function* fetchCountry() {}

続いて上記で作成したAPIを叩く関数のrequestCountryApiを実行する処理を追記します。

country/saga.js
import { put, call, takeLatest } from 'redux-saga/effects'

function* fetchCountry() {
  const { country, error } = yield call(requestCountryApi)
}

yieldは非同期処理を同期的におこなうようなものです。これを使用して、requestCountryApiをcallコマンドに渡してあげます。yield call(requestCountryApi)を実行することにより、返り値が { country } または { error } であったので、 const { country, error } = yield call(requestCountryApi) と書いてあげます。これでAPIを実行した結果を同期的に変数に持たせることができました。

続いて分岐処理を書いていきます。

country/saga.js
import { put, call, takeLatest } from 'redux-saga/effects'

function* fetchCountry() {
  const { country, error } = yield call(requestCountryApi)

  if (country) {
    // 成功時のアクション
  } else {
    // 失敗時のアクション
  }
}

countryにデータがあれば接続成功、なければ接続失敗なのでif文でそのように処理を分岐させます。この時、成功なら成功時のアクション、失敗なら失敗時のアクションを実行します。それでは作成したアクションをもう一度確認しましょう。

country/action.js
export const successCountryApi = (response) => {
  return {
    type: SUCCESS_COUNTRY_API,
    items: response
  }
}

export const failCountryApi = (error) => {
  return {
    type: FAIL_COUNTRY_API,
    items: error
  }
}

successCountryApiには国名データを渡します。
failCountryApiにはエラーデータを渡します。

この二つのアクションを実行する処理内容は下記になります。

country/saga.js
import { FETCH_COUNTRY, successCountryApi, failCountryApi } from './action'

function* fetchCountry() {
  const { country, error } = yield call(requestCountryApi)

  if (country) {
    yield put(successCountryApi(country))
  } else {
    yield put(failCountryApi(error))
  }
}

ここでもyieldをそれぞれのアクションに使用します。そしてアクションを実行するためにputコマンドにアクション関数とそれぞれのデータを渡してあげます。これでジェネレータ関数の中身は書けました。

最後に、FETCH_COUNTRYのアクションを実行すればジェネレータ関数 fetchCountry も動く、という処理を下記のように書きます。

country/saga.js
export const countrySaga = [takeLatest(FETCH_COUNTRY, fetchCountry)]

全体コードは下記になります。

country/saga.js
import axios from 'axios'
import { put, call, takeEvery, takeLatest } from 'redux-saga/effects'
import { FETCH_COUNTRY, successCountryApi, failCountryApi } from './action'

const requestCountryApi = () => {
  const url = 'https://restcountries.eu/rest/v2/all'
  return axios
    .get(url)
    .then((response) => {
      const country = response.data.map((item) => item.name)
      return { country }
    })
    .catch((error) => {
      return { error }
    })
}

function* fetchCountry() {
  const { country, error } = yield call(requestCountryApi)

  if (country) {
    yield put(successCountryApi(country))
  } else {
    yield put(failCountryApi(error))
  }
}

export const countrySaga = [takeLatest(FETCH_COUNTRY, fetchCountry)]

これでsagaの処理内容を書けました!
また最後のcountrySagaは後ほど使用するのでexportしております。

それではreducerを作成していきましょう。

reducerの作成

country/reducer.js
import { FETCH_COUNTRY, SUCCESS_COUNTRY_API, FAIL_COUNTRY_API } from './action'

const initialState = {
  type: '',
  items: []
}

const countryState = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_COUNTRY:
      state.type = action.type
      state.items = action.items
      return Object.assign({}, state)
    case SUCCESS_COUNTRY_API:
      state.type = action.type
      state.items = action.items
      return Object.assign({}, state)
    case FAIL_COUNTRY_API:
      state.type = action.type
      state.items = action.items
      return Object.assign({}, state)
    default:
      return state
  }
}

export default countryState

reducerはswitch文でアクション別に処理内容を変えてあげ、そのデータをreturnすれば良いです。今回reducerは本題ではないので詳しく解説しませんが、引数のactionにはそれぞれ作成したアクション関数の返り値が入っています。それを使用して、stateの引数に再代入し、データを返却してます。最後にその処理を countryState という変数に入れ、exportしています。

これで!国情報データAPI処理をredux-sagaで書くことができました!
最後にこれら作成したものを動かすための設定をします!もう一息です。

redux-sagaを使用したRedux設定をしていこう!

まず、redux-sagaを一つにまとめるための処理を書きます。今回はcountrySaga一つしか作成していませんが、これが hogeSaga fugaSaga という感じで複数増えていくはずです。それに対応するための処理を追記していきます。

ルート用の saga.js ファイルを作成して、下記のように記述します。

saga.js
import { all } from 'redux-saga/effects'
import { countrySaga } from './country/saga'

export default function* rootSaga() {
  yield all([...countrySaga])
}

ジェネレータ関数のrootSagaを作成し、その中で先ほど作成した countrySaga を実行しています。この時、effectを並列処理するallコマンドを使用します。これで rootSaga が作成できました。

ちなみに、複数のsagaを登録する書き方は下記のようになります。

saga.js
yield all([...countrySaga, ...hogeSaga, ...fugaSaga])

配列のように追記していくだけですね。

続いてreducerも一つにまとめる処理を書いていきましょう。

reducer.js
import { combineReducers, Reducer } from 'redux'
import countryState from './country/reducer'

const reducer = combineReducers({ countryState })

export default reducer

combineReducers を使用すれば、複数のreducerを登録できます。複数登録したい場合は combineReducers({ countryState, hogeState, fugaState }) みたいに書いていけば良いです。

続いてstoreを作成していきます。redux-sagaを使用したstoreの作成方法は通常のものと少し異なります。

store.js
import { createStore, applyMiddleware, Store, AnyAction } from 'redux'
import createSagaMiddleware, { SagaMiddleware } from 'redux-saga'
import { composeWithDevTools } from 'redux-devtools-extension'
import reducer from './reducer'
import rootSaga from './saga'

const sagaMiddleware = createSagaMiddleware()

const configureStore = () => {
  const store = createStore(reducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))
  sagaMiddleware.run(rootSaga)

  return store
}

const store = configureStore()

export default store

公式を見たりググったりして書いてあった内容を書いているので、はっきり言って中身がどうなっているかは曖昧な理解です。ちなみに composeWithDevTools はChromeの開発者モードでreduxの中身が確認できるので便利です!

これでReduxの設定ができました!このstoreは下記のようにルートに渡してあげることで実行できます。

index.jsx
import React from 'react'
import ReactDOM from 'react-dom' 
import { Provider } from 'react-redux'
import App from './App.jsx'
import store from './store'

const app = document.getElementById('app')

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  app
)

そして作成したreduxのAPI処理を実行したいときは、アクションで作成した fetchCountry を dispatchすることで実行できます。 dispatch(fetchCountry()) という書き方です。

例えば、App.jsxに下記のようなものを書きます。

App.jsx
import React from 'react'
import store from './store'
import { fetchCountry } from './country/action'

export default class App extends React.Component {
  public constructor(props) {
    super(props)
    this.state = {
      countries: []
    }
  }

  protected get countryLists() {
    return this.state.countries.map(country => {
      return <li key={country}>{country}</li>
    })
  }

  // storeが更新されるのを確認します
  protected unsubscribe() {
    store.subscribe(() => {
      this.setState({
        countries: store.countryState.items
      })
    })
  }

  async componentDidMount() {
    await dispatch(fetchCountry())
  }

  public render() {
    this.unsubscribe()
    return (
      <ul>
        {this.countryLists}
      </ul>
    )
  }
}

componentDidMount() {} のなかでdipatchして、更新したデータを使用して国情報をliタグで突っ込んで表示する処理内容です。動作確認をしていないので動くかどうかは保証できません。。。

まとめ

長くなりましたが、redux-sagaを使用したAPI処理とRedux作成の内容は以上となります。
redux-sagaを使用すれば、actionとAPI処理を別々に分けて書くことができるので、テストする際もAPI接続テスト、アクションテスト、という風に分けることができ、スッキリした構造を作成できるかと思います。

またTypeScriptで作成することはもちろん可能です。型をつけてあげれば良いだけです。
SPAサイトではAPI処理をすることも多いかと思いますので、redux-sagaを使用してMiddleWareをシンプルに作成していきましょう。

niyou0ct
fork
株式会社フォークは、Webサイトの企画・制作・開発・サーバホスティング・コンタクトセンターを一社に集約したワンストップソリューションを展開する制作会社です。
https://www.fork.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away