React

Redux-sagaで簡単なAPIを動かしてみる

React
この記事は約13分で読めます。

はじめに

Reduxを実装したりtoolkitを使っているとAPIと通信しなければならない時があります。
なので簡単なAPIを使ってなるべく簡潔に動作させてみたいと思います。
React Nativeで実装していきます

実現すること

  • 犬と猫の画像をランダムで表示するだけのAPIと通信する
  • 必要なAPIの数が多くなっても管理しやすい形にする

画面実装

この2記事で実装した画面を使いまわします。
上から順にコピペしていくだけでOKです

React NativeでNavigationを使っていい感じに画面遷移する

Redux toolkitでReduxをなるべく簡単に実装する

ライブラリをインストールする

公式ドキュメントに書いてあるredux-sagaに加えてaxiosというライブラリもインストールします

yarn add redux-saga
yarn add axios

入れたライブラリはこんな感じです

ライブラリ用途
redux-sagaredux-saga本体
axiosサーバーからデータを取得する

ライブラリ入れたらpod installもとりあえずやりましょう

cd ios && pod install && cd ..
react-native run-ios

これで下準備が完了!!

sliceのファイルにactionを追加する

まず最初にuserSliceの中に犬の画像をgetするための記述を追加します
payloadのところにはアクションの実行に必要な任意のデータを入れます

userSlice.js

import {createSlice} from '@reduxjs/toolkit';

const userInitialState = {
  userName: null,
  userEmail: null,
  // stateを追加
  img: null,
};

export const userSlice = createSlice({
  name: 'user',
  initialState: userInitialState,
  reducers: {
    getInfo: state => {
      state.userName = 'Tanaka Taro';
      state.userEmail = 'practice@example.com';
    },
    resetInfo: state => {
      state.userName = userInitialState.userName;
      state.userEmail = userInitialState.userEmail;
    },
    // payloadしてimgに画像を入れる
    getImg: (state, {payload}) => {
      state.img = payload;
    },
  },
});

export const {getInfo, resetInfo, getImg} = userSlice.actions;

ファイルを追加する

userディレクトリ配下にAPIと繋ぐためのuserSaga.jsを用意します(userSliceと同じディレクトリに配置)

applyAxiosの中には呼び出したいAPI(url)があって、
そこから返ってきた値をcallApiでレスポンスとして受け取っているイメージです。

また、sagaはeffectという「非同期処理を同期的に行うための関数」を使用します。
put , call , takeLatestがその関数にあたります
参考記事

userSaga.js

import axios from 'axios';
import {put, call, takeLatest} from 'redux-saga/effects';
import {getImg} from './userSlice';

function applyAxios() {
  const url = 'https://dog.ceo/api/breeds/image/random';
  return axios.get(url);
}

function* callApi() {
  try {
    const res = yield call(applyAxios);
    yield put(getImg(res));
  } catch (error) {
    console.log(error);
  }
}

export const getDog = () => ({type: 'GET_DOG'});

export function* userSaga() {
  yield takeLatest('GET_DOG', callApi);
}

todoの方にも反映させる

同様にtodoSliceも修正し、同じディレクトリ内にtodoSagaも作成しましょう

todoSlice.js

import {createSlice} from '@reduxjs/toolkit';

const todoInitialState = {
  userTodo: null,
  img: null,
};

export const todoSlice = createSlice({
  name: 'todo',
  initialState: todoInitialState,
  reducers: {
    setTodo: state => {
      state.userTodo = 'jogging';
    },
    resetTodo: state => {
      state.userTodo = todoInitialState.userTodo;
    },
    getImg: (state, {payload}) => {
      state.img = payload;
    },
  },
});

export const {setTodo, resetTodo, getImg} = todoSlice.actions;
  • stateを追加
  • getImgでimgをpayloadする

これらの記述を追加しました

todoSaga.js

import axios from 'axios';
import {put, call, takeLatest} from 'redux-saga/effects';
import {getImg} from './todoSlice';

function applyAxios() {
  const url = 'https://aws.random.cat/meow';
  return axios.get(url);
}

function* callApi() {
  try {
    const res = yield call(applyAxios);
    yield put(getImg(res));
  } catch (error) {
    console.log(error);
  }
}

export const getCat = () => ({type: 'GET_CAT'});

export function* todoSaga() {
  yield takeLatest('GET_CAT', callApi);
}

各sagaをまとめるファイルを作る

redux配下にsagas.jsを作成し、その中に作成したsagaをまとめたいと思います
(これによりAPIが増えてもファイル1つで管理できる)

sagas.jsの中ではyield allを使って各sagaをまとめます

sagas.js

import {all} from 'redux-saga/effects';
import {userSaga} from './user/userSaga';
import {todoSaga} from './todo/todoSaga';

export default function* rootSaga() {
  yield all([userSaga(), todoSaga()]);
}

storeを生成する

個人的にこれはもう暗記しちゃった方が手っ取り早いと思ってます。
さっき作ったsagas.jsの中のrootSagaを、storeに結合して使えるようにする処理です

store.js

import {configureStore} from '@reduxjs/toolkit';
import {rootReducer} from './reducers';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware],
});

sagaMiddleware.run(rootSaga);

export default store;

これでRedux-sagaの大まかな実装は終わりです

画面に反映させる

実際に画面上でAPIを受け取って画像を受け取りましょう
dispatchしてAPIを叩き、返ってきた画像を受け取るための処理を追加します

HomeScreen.js

import React from 'react';
import {StyleSheet, View, Text, Button, Image} from 'react-native';
import {getInfo, resetInfo} from '../redux/user/userSlice';
// userSagaから該当するAPIをimport
import {getDog} from '../redux/user/userSaga';
import {useSelector, useDispatch} from 'react-redux';

const HomeScreen = ({navigation}) => {
  const dispatch = useDispatch();
  const user = useSelector(state => state.user);

  // dogというstateがpayloadされた時に入る画像URL
  const dogImage = user.img?.data.message;

  return (
    <View style={styles.container}>
      <Text>私の名前は{user.userName}</Text>
      <Text>メールアドレスは{user.userEmail}</Text>
      <Image
        style={styles.img}
        source={{
          uri: dogImage,
        }}
      />
      <Button
        title="セットする"
        onPress={() => {
          dispatch(getInfo());
        }}
      />
      <Button
        title="リセットする"
        onPress={() => {
          dispatch(resetInfo());
        }}
      />
      <Button
        title="犬を取得"
        onPress={() => {
          dispatch(getDog());
        }}
      />
      <Button
        title="画面遷移する"
        onPress={() => {
          navigation.navigate('Detail');
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  input: {
    width: '50%',
    borderWidth: 1,
  },
  // 画像表示用のstyle
  img: {
    width: '75%',
    height: '50%',
  },
});

export default HomeScreen;

犬を取得ボタンを押下した時、犬が表示されたかと思います。

とても可愛らしいですね。

DetailScreenでも同じことをしましょう
getCatをdispatchし、帰ってきた値をImageコンポーネントのuriに入れます

DetailScreen.js

import React from 'react';
import {StyleSheet, View, Text, Button, Image} from 'react-native';
import {setTodo, resetTodo} from '../redux/todo/todoSlice';
import {getCat} from '../redux/todo/todoSaga';
import {useSelector, useDispatch} from 'react-redux';

const DetailScreen = ({navigation}) => {
  const dispatch = useDispatch();
  const todo = useSelector(state => state.todo);

  const catImage = todo.img?.data.file;

  return (
    <View style={styles.container}>
      <Text>明日のTodoは{todo.userTodo}</Text>
      <Image
        style={styles.img}
        source={{
          uri: catImage,
        }}
      />
      <Button
        title="セットする"
        onPress={() => {
          dispatch(setTodo());
        }}
      />
      <Button
        title="リセットする"
        onPress={() => {
          dispatch(resetTodo());
        }}
      />
      <Button
        title="猫を取得"
        onPress={() => {
          dispatch(getCat());
        }}
      />
      <Button
        title="Home画面に遷移する"
        onPress={() => {
          navigation.navigate('Home');
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  img: {
    width: '75%',
    height: '50%',
  },
});

export default DetailScreen;

猫の画像が表示されたかと思います

僕の実家には茶色い猫と白い猫の2匹がいます。

駆け足で説明しましたがこれでsagaの大まかな実装が完了しました

最後に

簡単なAPIを繋いで画面に反映させるところまでを実装しました。
命名規則に一部統一感のない部分がありますが、sagas.jsでそれぞれのsagaをまとめておくことで通信するAPIが多くなったとしても一元的に管理ができるかと思います。

参考

https://qiita.com/macotok/items/ec5460ac17f5a20c4735
https://dev.classmethod.jp/articles/redux-saga-cheatsheet/