React から axios で Rails API を叩く実装

公開:2020/09/29
更新:2020/09/29
6 min読了の目安(約6100字TECH技術記事

Railsで動く既存サービスを全面React化した理由と方法の続編になります。


こんにちは。
Rep立教大学シラバス検索システムを運営している sugiken です。
Railsで動く既存サービスを全面React化した理由と方法では React 化をした理由と方法について書きました。

今回は一気に具体性を増して、実装の話多めで React から API を叩く部分の実装について書きます。
GET 時の Canceler と POST 時の config の切り替え辺りをちょっと工夫したのでぜひ読んでみてください!

正直 JS 初心者過ぎるので、おかしな部分がありましたらコメント頂けたらと思います。勉強させてください。

使用技術

フロントエンド

  • React 16.13.0
  • axios 0.19.2
  • TypeScript

サーバーサイド

  • Rails 5.1系
  • rack-cors

やりたいこと

  • フロント(web.rep-rikkyo.com)から web API(www.rep-rikkyo.com)を叩く
  • GET, POST, PUT, DELETE などそれぞれのメソッドで叩く
  • 認証に関する情報はリクエストヘッダーに載っける

Rails側の対応

ざっくり説明します。
今回は既存の Rails のみで動いていたサービスからフロント部分を切り出したので、Rails は API モードではありませんでした。
そこで クロスオリジン間でのリクエストを可能にする rack-cors gem を install します。

Gemfile に以下を追記

gem 'rack-cors'

config/initializers/cors.rb を作成し、以下のようにすることで https://web.rep-rikkyo.com からのリクエストを受け取れるようにします。

# frozen_string_literal: true

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://web.rep-rikkyo.com'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head],
             credentials: true,
             expose: %w[access-token uid client expiry]
  end
end

この origins を「とりあえず*にしましょう」みたいな記事もありますが、どこからでも叩かれるなんて怖すぎるので、ちゃんと指定しましょう。
また、origins に配列で複数のドメインを渡している記事もありますが、stringしか受け付ないので、origins ['https://a.rep-rikkyo.com', 'https://b.rep-rikkyo.com'].map(&:strip) のように無理やり文字列化しましょう。

その他 controller の API 化や protect_from_forgery などの設定は適宜必要だと思いますが割愛します。

React側

ようやく本題。
API を叩くのは様々な component から使用する予定ですので抽象化は必須です。

axiosInstance の初期化

axios の調査をしていると、axios.create(config) により axiosInstance を作成できることが分かりました。
対象のAPIドメインが1つで、すべての HTTP メソッドで同じ config を渡せばいいので、まずは axiosInstance を初期化する関数を作成しました。

import axios from 'axios';

const DEFAULT_API_CONFIG: ApiConfig = {
  baseURL: 'https://www.rep-rikkyo.com',
  timeout: 5000,
  mode: 'cors',
  credentials: 'include',
  headers: {
    ContentType: 'application/json',
    Accept: 'application/json',
  },
};

const newAxiosInstance = () => {
  const instance = axios.create(DEFAULT_API_CONFIG);

  instance.interceptors.response.use(
    response => {
      if (process.env.NODE_ENV === 'development') {
        console.log(response); // 便利☆
      }
      return response;
    },
    error => {
      return Promise.reject(error);
    }
  );

  return instance;
};

GET を叩く関数

まずはコードから。
* fetch という関数名は JS 標準の fetch とかぶるので良くなかったなと感じています。

interface Options {
  cancelToken?: CancelToken;
}

export const fetch = async (
  path: string,
  query?: Object,
  options?: Options
) => {
  const instance = newAxiosInstance(options);

  try {
    const response = await instance.get(path, { params: query });
    return response;
  } catch (error) {
    return error.response;
  }
};

関数内では先ほど作成した newAxiosInstance 関数を利用して axios の get を実行しているだけです。

第1引数: path

叩く api の path を表していて、'/api/v1/lessons/100' のような文字列を渡します。

第2引数: query

GET ですので、リクエストパラメータに付与する Object を渡します。Repでは主に検索のパラメータが渡ってくることが多いです。

第3引数: options の CancelToken について

axios の機能である Canceler を利用することでリクエストをキャンセルすることができます。
これはリクエストからレスポンスまでが長くなる可能性のある場合に有効です。
例えばRepの授業検索では、以下のように Canceler が機能します。

  1. 時間がかかるような検索条件でAPIを叩く
  2. 直後に時間のかからない軽い検索を実行する
  3. 1のリクエストをキャンセルする
  4. 2のリクエストが反映される

先のリクエストがキャンセルされた例
先のリクエストがキャンセルされていることが分かります。

そしてこれを利用できるように newAxiosInstance も書き換えます。

const newAxiosInstance = (options?: Options) => {
  const config = DEFAULT_API_CONFIG;
  const configWithCancelToken = { ...config, cancelToken: options?.cancelToken }

  const instance = axios.create(configWithCancelToken);
  ・・・
}

Canceler の使い方

シンプルに1つ前の cancelToken を変数に保存しておき、2回目以降のリクエスト前に cancelToken がある場合は cancel() します。

import axios, { Canceler } from 'axios';

let CancelToken = axios.CancelToken;
let cancel: Canceler | null;

const SearchLesson: React.FC = () => {
  const searchLesson = async (query: SearchQuery) => {
    if (cancel) {
      // fetchのcancelerがすでにある場合 = 一度fetchしようとしたが、まだ完了していないときはfetchをキャンセルする
      cancel();
      cancel = null;
    }
    const res = await fetch('/lesson/search', query, {
      cancelToken: new CancelToken(function executor(c) {
        cancel = c;
      }),
    });
    cancel = null;
  };
  ・・・
}

POST を叩く関数

ほとんど GET と同じですが、Rep では POST の際に認証状態を確認する API がほとんどのためその対応をします。

interface Options {
  isAuth?: boolean; // 追加
  cancelToken?: CancelToken;
}

export const post = async (
  path: string,
  query?: Object,
  options?: Options
) => {
  const instance = newAxiosInstance(options);

  try {
    const response = await instance.post(path, query);
    return response;
  } catch (error) {
    return error.response;
  }
};

Options に isAuth を追加しました。
これに合わせて newAxiosInstance も修正します。

引数の options.isAuth に合わせて API config を切り替えるようにしました。

export const AUTHED_API_CONFIG = (): ApiConfig => {
  return {
    baseURL: 'https://www.rep-rikkyo.com',
    timeout: 5000,
    mode: 'cors',
    credentials: 'include',
    headers: {
      ContentType: 'application/json',
      Accept: 'application/json',

      // 認証情報をヘッダーに載っける
      'access-token': authInfo().Token,
      client: authInfo().Client,
      uid: authInfo().Uid,
    },
  }
};

const newAxiosInstance = (options?: Options) => {
  const isAuth = options ? options.isAuth : false; // 追加
  const config = isAuth ? AUTHED_API_CONFIG() : DEFAULT_API_CONFIG; // 追加
  const configWithCancelToken = { ...config, cancelToken: options?.cancelToken }

  const instance = axios.create(configWithCancelToken);
  ・・・
}

POST を実行する箇所は GET の Canceler ほど複雑なことをしていないので割愛します。


最後に

Rails に rack-cors を導入して、axios で API を叩いてみました。
GET の Canceler と POST 時の config の切り替え辺りがちょっと工夫したことです。

*実装時にいくつかの記事を読んで真似た部分も多々あるのですが、申し訳ないことにソースを探しても見つかりませんでしたmm
心当たりある方はコメントください。

今回はコード多めにかけたぞ!