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を作ります。
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に保存したいデータを入れたオブジェクト
を返しています。引数にresponse
、error
と置いているので、それぞれのアクションを実行する時に、渡したいデータを渡してあげると良いです。今回はresponse
に国名だけ入った配列データを渡して、items
に入れてあげる処理をしたいとおもいます。
アクションは準備ができました!
続いてMiddleWareの処理を書くためにAPI処理と今回の本題であるredux-sagaを書いていきましょう!
API処理の関数を作成する
最初にAPIを実行する関数を作ってあげましょう。API実行関数を別ファイル化する場合もありますが、今回は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という名前で作成しましょう。
function* fetchCountry() {}
続いて上記で作成したAPIを叩く関数のrequestCountryApi
を実行する処理を追記します。
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を実行した結果を同期的に変数に持たせることができました。
続いて分岐処理を書いていきます。
import { put, call, takeLatest } from 'redux-saga/effects'
function* fetchCountry() {
const { country, error } = yield call(requestCountryApi)
if (country) {
// 成功時のアクション
} else {
// 失敗時のアクション
}
}
countryにデータがあれば接続成功、なければ接続失敗なのでif文でそのように処理を分岐させます。この時、成功なら成功時のアクション、失敗なら失敗時のアクションを実行します。それでは作成したアクションをもう一度確認しましょう。
export const successCountryApi = (response) => {
return {
type: SUCCESS_COUNTRY_API,
items: response
}
}
export const failCountryApi = (error) => {
return {
type: FAIL_COUNTRY_API,
items: error
}
}
successCountryApi
には国名データを渡します。
failCountryApi
にはエラーデータを渡します。
この二つのアクションを実行する処理内容は下記になります。
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
も動く、という処理を下記のように書きます。
export const countrySaga = [takeLatest(FETCH_COUNTRY, fetchCountry)]
全体コードは下記になります。
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の作成
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
ファイルを作成して、下記のように記述します。
import { all } from 'redux-saga/effects'
import { countrySaga } from './country/saga'
export default function* rootSaga() {
yield all([...countrySaga])
}
ジェネレータ関数のrootSagaを作成し、その中で先ほど作成した countrySaga
を実行しています。この時、effectを並列処理するallコマンドを使用します。これで rootSaga
が作成できました。
ちなみに、複数のsagaを登録する書き方は下記のようになります。
yield all([...countrySaga, ...hogeSaga, ...fugaSaga])
配列のように追記していくだけですね。
続いてreducerも一つにまとめる処理を書いていきましょう。
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の作成方法は通常のものと少し異なります。
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は下記のようにルートに渡してあげることで実行できます。
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に下記のようなものを書きます。
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をシンプルに作成していきましょう。