React + Redux + TypeScript でモダンなwebアプリを作るチュートリアルを考えてみた②

# :star: はじめに
- この記事は **[React + Redux + TypeScript でモダンなwebアプリを作るチュートリアルを考えてみた①](https://qiita.com/mikan3rd/items/2576c2f993d9c28d744c)** の続きです
- GitHubのリポジトリはこちら
  - https://github.com/mikan3rd/react-tutorial


## :pencil: GoogleBooksAPIを試してみる
- GoogleBooksAPIは登録なしで使用できる数少ない公開APIです
- 公式
  - https://developers.google.com/books
- 例えば、本の検索は https://www.googleapis.com/books/v1/volumes?q=ジョジョ のように簡単に実行できます
- 公式リファレンス `Volume: list`
  - https://developers.google.com/books/docs/v1/reference/volumes/list
  - 右側の `Try this API` からも様々な条件をつけて検索を試すことができます

![developers.google.com_books_docs_v1_reference_volumes_list_apix_params=%7B%22q%22%3A%22%E3%82%B8%E3%83%A7%E3%82%B8%E3%83%A7%22%7D(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/36320a3f-d9be-c44b-89b1-667eb188fc46.png)

## :pencil: axiosでAPIにリクエストを送る
- axiosを使うとHTTP通信を簡単に実装することができます
- まずはaxiosを追加しましょう
- 公式
  - https://github.com/axios/axios

```bash:terminal
yarn add axios
```

- 先ほどのGoogleBooksのAPIを叩く関数 `searchGoogleBooks()` を作成してみましょう
- アロー関数を使うことでより短く簡潔に関数リテラルを定義できます
  - ここでは出てきませんが `this` を束縛できるという特徴もあります
- Promiseによる非同期処理には `async` `await` を使うと簡単に書けます
  - アロー関数で使用する場合は引数の直前に `async` を記述します
- `axios.get()` のようにHTTPリクエストを送ることができます

```tsx:Otameshi.tsx
import axios from 'axios';

const searchGoogleBooks = async (searchString: string) => {
  const url = 'https://www.googleapis.com/books/v1/volumes';
  const params = { q: searchString };
  try {
    const response = await axios.get(url, { params });
    return { isSuccess: true, data: response.data, error: null };
  } catch (error) {
    return { isSuccess: false, data: null, error };
  }
};
```

- Inputに入力した文字をクエリとし、ボタンをクリックすると検索を行うように変更してみます

```tsx:Otameshi.tsx
export const Otameshi: React.FC = () => {
  const [searchString, changeSearchString] = useState('');

  const handleOnSearchButton = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    // form要素のbuttonのsubmitを止める
    event.preventDefault();
    const result = await searchGoogleBooks(searchString);
  };

  return (
    <Wrapper>
      <Body>
        <Title>Google Books 検索</Title>
        <SearchForm>
          <Input placeholder='検索ワードを入力してね！' onChange={event => changeSearchString(event.target.value)} />
          <SearchButton onClick={event => handleOnSearchButton(event)} disabled={!searchString}>
            検索
          </SearchButton>
        </SearchForm>
      </Body>
    </Wrapper>
  );
};

const Wrapper = styled.div`
  display: flex;
  justify-content: center;
  margin-top: 20px;
`;

const Body = styled.div``;

const Title = styled.h1`
  font-size: 24px;
  font-weight: bold;
  text-align: center;
`;

const Input = styled.input`
  display: block;
  box-sizing: border-box;
  width: 250px;
  font-size: 18px;
  padding: 10px;
  outline: none;
`;

const SearchForm = styled.form`
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 20px;
`;

const SearchButton = styled.button`
  color: #fff;
  background-color: #09d3ac;
  border-radius: 3px;
  margin-left: 10px;
  padding: 10px;
  font-size: 18px;
  border: none;
  outline: none;
  transition: 0.4s;
  cursor: pointer;
  &:disabled {
    background-color: #bfbfbf;
    cursor: not-allowed;
  }
`;

```

- Chromeのディベロッパーツールなどでレスポンスが正常に返ってきていることを確認できます

<img width="818" alt="スクリーンショット 2019-12-09 23.07.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/73970dd9-b826-2523-bc90-f8513dfaf11d.png">


- レスポンスを表示するように変更しましょう
- `useState` でレスポンスを保存するstate `searchResult` を新しく定義しています
- `searchResult` に値が入っている場合のみ、 `searchResult` を表示するようにしています
- Reactでは `map` のように繰り返しを使って要素を返す場合、 `key` にユニークな値を指定する必要があります

```tsx:Otameshi.tsx
export const Otameshi: React.FC = () => {
  const [searchString, changeSearchString] = useState('');
  const [searchResult, changeSearchResult] = useState<any>(null);

  const handleOnSearchButton = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    // form要素のbuttonのsubmitを止める
    event.preventDefault();
    const result = await searchGoogleBooks(searchString);
    if (result.isSuccess) {
      changeSearchResult(result.data);
    } else {
      window.alert(String(result.error));
    }
  };

  return (
    <Wrapper>
      <Body>
        <Title>Google Books 検索</Title>

        <SearchForm>
          <Input placeholder='検索ワードを入力してね！' onChange={event => changeSearchString(event.target.value)} />
          <SearchButton onClick={event => handleOnSearchButton(event)} disabled={!searchString}>
            検索
          </SearchButton>
        </SearchForm>

        {searchResult && (
          <ResultContent>
            {searchResult.items.map((item: any) => {
              return <ResultTitle key={item.id}>{item.volumeInfo.title}</ResultTitle>;
            })}
          </ResultContent>
        )}
      </Body>
    </Wrapper>
  );
};

const ResultContent = styled.div`
  margin-top: 20px;
`;

const ResultTitle = styled.div`
  padding: 10px 0;
  border-bottom: 1px solid;
  &:first-of-type {
    border-top: 1px solid;
  }
`;

```

- 次のように検索結果を表示できるようになりました

![localhost_3000_otameshi_(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/631c541e-55fb-1c68-0a5b-9c1d2df215da.png)

- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/9a41f4098df3706e23e73b31cc124a5c4a522e20

## :pencil: immutable.jsでレスポンスをモデル化する
- このままだと `searchResult` の中身が不明確で、また想定外のデータが入ってしまうことが防げません
- ここで、`immutable.js` の`Record`を使ってモデル化することで、特定のkeyを必ず持つ不変オブジェクトとして取り扱うことができるようになります
- 公式
  - https://immutable-js.github.io/immutable-js/

- また、日時操作のライブラリとして `dayjs` を使用するため追加しておきます
  - 参考: [日付時刻操作ライブラリをmomentからdayjsへ乗り換えた](https://qiita.com/yagi_suke/items/2848c8981ea6d9f26587)

```bash:terminal
# TypeScriptに対応するためv4を使用する
yarn add immutable@^4.0.0-rc.12

yarn add dayjs
```

- Google Books APIのリファレンスを参考にVolumeモデルを作成します
  - https://developers.google.com/books/docs/v1/reference/volumes

- 今回はレスポンスと同じ構造のモデルを`immutable.js`の`Record`で作成しています
  - 各Recordに `fromResponse` というレスポンスからモデルを生成するstaticメソッドを作っています
  - ネストする場合、`Map`を使うと任意のkeyを追加できてしまうため、ここでもRecordを使うようにしています
  - 配列の場合は`List`を使うようにしています

```ts:src/models/Volume.ts
import { List, Record } from 'immutable';
import dayjs, { Dayjs } from 'dayjs';

import { JSObject } from 'types/Common';

export class ImageLinks extends Record<{
  smallThumbnail: string;
  thumbnail: string;
}>({
  smallThumbnail: '',
  thumbnail: '',
}) {
  static fromResponse(response: JSObject) {
    const params = { ...response };
    return new ImageLinks(params);
  }
}

export class VolumeInfo extends Record<{
  title: string;
  subtitle: string;
  authors: List<string>;
  publisher: string;
  publishedDate: Dayjs;
  description: string;
  imageLinks: ImageLinks;
  previewLink: string;
  infoLink: string;
  canonicalVolumeLink: string;
}>({
  title: '',
  subtitle: '',
  authors: List(),
  publisher: '',
  publishedDate: dayjs(),
  description: '',
  imageLinks: new ImageLinks(),
  previewLink: '',
  infoLink: '',
  canonicalVolumeLink: '',
}) {
  static fromResponse(response: JSObject) {
    const params = { ...response };
    params.authors = List(params.authors);
    params.publishedDate = dayjs(params.publishedDate);
    params.imageLinks = ImageLinks.fromResponse(params.imageLinks);
    return new VolumeInfo(params);
  }
  get descriptionWithNewLine() {
    return this.description.replace('。 ', '\n');
  }
  get publishedDateString() {
    return this.publishedDate.format('YYYY/MM/DD');
  }
}

export class Volume extends Record<{
  id: number;
  selfLink: string;
  volumeInfo: VolumeInfo;
}>({
  id: 0,
  selfLink: '',
  volumeInfo: new VolumeInfo(),
}) {
  static fromResponse(response: JSObject) {
    const params = { ...response };
    params.volumeInfo = VolumeInfo.fromResponse(params.volumeInfo);
    return new Volume(params);
  }
}
```

- さらに`Volume: list`のレスポンスもモデル化します
  - https://developers.google.com/books/docs/v1/reference/volumes/list

```ts:src/models/Volume.ts
export class VolumeList extends Record<{
  kind: string;
  totalItems: number;
  items: List<Volume>;
}>({
  kind: '',
  totalItems: 0,
  items: List(),
}) {
  static fromResponse(response: JSObject) {
    const params = { ...response };
    params.items = List(params.items.map((item: JSObject) => Volume.fromResponse(item)));
    return new VolumeList(params);
  }
}

```
- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/748eee8f57c530f9aad2c5c392728c6ec142916b


- 実際に`searchResult`とAPIのレスポンスをモデルに置き換えてみましょう
  - Type付きでモデル化していればエディターのサジェストも出てくるので便利です

```tsx:Otameshi.tsx
import { VolumeList } from 'models/Volume';

export const Otameshi: React.FC = () => {
  const [searchString, changeSearchString] = useState('');
  const [searchResult, changeSearchResult] = useState<VolumeList>(new VolumeList());

  const handleOnSearchButton = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    // form要素のbuttonのsubmitを止める
    event.preventDefault();

    const result = await searchGoogleBooks(searchString);
    if (result.isSuccess) {
      changeSearchResult(VolumeList.fromResponse(result.data));
    } else {
      window.alert(String(result.error));
    }
  };

  return (
    <Wrapper>
      <Body>
        <Title>Google Books 検索</Title>

        <SearchForm>
          <Input placeholder='検索ワードを入力してね！' onChange={event => changeSearchString(event.target.value)} />
          <SearchButton onClick={event => handleOnSearchButton(event)} disabled={!searchString}>
            検索
          </SearchButton>
        </SearchForm>

        {searchResult.kind && (
          <ResultContent>
            {searchResult.items.map(item => {
              return <ResultTitle key={item.id}>{item.volumeInfo.title}</ResultTitle>;
            })}
          </ResultContent>
        )}
      </Body>
    </Wrapper>
  );
};
```
- これでレスポンスのデータも型安全に取り扱えるようになりました
- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/09c6e1bc5d968e89a37921713f636ae4b9848843

## :pencil: Reduxを導入する
- Reduxを導入することでReactコンポーネントのファイルからロジックを分けて管理したり、別々のコンポーネントで同じstateを共通利用したりできるようになります
- 公式
  - https://redux.js.org/
  - Reduxには `Store` `Action` `Reducer` といった様々な要素が登場しますが、ここでは説明を省略するので初めて触れる方は他の記事など事前に目を通しておくことを推奨します
- まずは関連ライブラリをinstallしましょう
  - `redux-logger` を使うとreduxのログをconsoleに出力することができます
  - `typescript-fsa` を使うとTypescript対応のRedux Actionを定義できます
  - `typescript-fsa-reducers` を使うとTypescript対応のRedux Reducerを定義できます
  - `connected-react-router` を使うことで`react-router` をReduxに対応できます

```bash:terminal
yarn add redux react-redux redux-logger typescript-fsa typescript-fsa-reducers connected-react-router
yarn add -D @types/react-redux @types/redux-logger
```

- 最初にReducerをまとめる `rootReducer` を定義します
  - `connectRouter` の引数に `history` が必要なので `rootReducer` の引数として渡せるようにしてあります

```ts:src/reducers/index.ts
import { History } from 'history';
import { combineReducers } from 'redux';
import { RouterState, connectRouter } from 'connected-react-router';

export interface State {
  router: RouterState;
}

export const rootReducer = (history: History) =>
  combineReducers({
    router: connectRouter(history),
  });

```

- 次にStoreを定義する `configureStore` を用意します
  - 各種middlewareを挟む場合もここで行います

```ts:src/configureStore
import { applyMiddleware, createStore } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import { createLogger } from 'redux-logger';
import { createBrowserHistory } from 'history';

import { State, rootReducer } from 'reducers';

const logger = createLogger();

export const history = createBrowserHistory();

export function configureStore(preloadedState?: State) {
  const middlewares = [routerMiddleware(history), logger];
  const middlewareEnhancer = applyMiddleware(...middlewares);
  const store = createStore(rootReducer(history), preloadedState, middlewareEnhancer);
  return store;
}

```

- `react-redux` の `Provider` を使うことでReduxのStoreにアクセスできるようにします
- また `Router` は `ConnectedRouter` に置き換えます

```tsx:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';

import { configureStore, history } from './configureStore';

import routes from 'routes';

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>{routes}</ConnectedRouter>
  </Provider>,
  document.getElementById('root'),
);

```

- これでReduxを使用する準備が整いました
- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/277d931a5cdef11a03b340ec240c326bcb68cb11

## :pencil: Redux Hooksを使う
- それでは実際にReduxを使った実装に置き換えてみましょう
- 今回は `searchResult` `changeSearchResult` をreduxに置き換えてみます
- まずはGoogleBooks用のreducer `googleBooksReducer`を作成します
  - ここでもimmutable.jsのRecordで状態を管理します
  - `GoogleBooksState` でこのreducerのモデルを定義し、 `reducerWithInitialState()` で初期値を定義します

```ts:src/reducers/googleBooks.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { Record } from 'immutable';

import { VolumeList } from 'models/Volume';

export class GoogleBooksState extends Record<{
  volumeList: VolumeList;
}>({
  volumeList: new VolumeList(),
}) {}

export const googleBooksReducer = reducerWithInitialState(new GoogleBooksState());
```

- 次にGoogleBooks用のActionを `GoogleBooksActions` として定義します

```ts:src/actions/googleBooks
import actionCreatorFactory from 'typescript-fsa';

import { VolumeList } from 'models/Volume';

const actionCreator = actionCreatorFactory('GoogleBooks');

export const GoogleBooksActions = {
  setVolumes: actionCreator<VolumeList>('setVolumes'),
};

```

- reducerに戻って `GoogleBooksActions.setVolumes` が`dispatch`された時にstateを更新して返すようにします
  - callbackには現在のstateとActionの引数がpayloadとして渡ってきます
  - stateは`GoogleBooksState`なので、このうち`volumeList`を新しい値に更新します
  - immutable.jsには `set` `update` といった新しい値を返す関数が用意されているので、ドキュメントを参考に使い分けましょう

```ts:src/reducers/googleBooks.ts
import { GoogleBooksActions } from 'actions/googleBooks';

export class GoogleBooksState extends Record<{
  volumeList: VolumeList;
}>({
  volumeList: new VolumeList(),
}) {}

export const googleBooksReducer = reducerWithInitialState(new GoogleBooksState())
  .case(GoogleBooksActions.setVolumes, (state, payload) => {
      return state.set('volumeList', payload);
    },
  );
```

- 最後に、コンポーネントから上記のstateやActionを呼び出して使用するように置き換えてみましょう
  - これまでは `react-redux` の `connect` でコンポーネントとreduxを結合していましたが、`v7.1.0`よりhooksが使えるようになったため、hooksで記述しています
  - `useSelector` では任意のstateを呼び出すことができるようになりました
  - `useDispatch` でdispatchを生成することで、`dispatch(GoogleBooksActions.setVolumes()` といったように任意のActionをdispatchできるようになりました

```tsx:src/components/GoogleBooks/index.tsx
import { useDispatch, useSelector } from 'react-redux';

import { State } from 'reducers';
import { GoogleBooksActions } from 'actions/googleBooks';

export const GoogleBooks: React.FC = () => {
  const [searchString, changeSearchString] = useState('');

  const { volumeList } = useSelector((state: State) => ({ volumeList: state.googleBooks.volumeList }));

  const dispatch = useDispatch();

  const handleOnSearchButton = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    event.preventDefault();
    const result = await searchGoogleBooks(searchString);
    if (result.isSuccess) {
      dispatch(GoogleBooksActions.setVolumes(VolumeList.fromResponse(result.data)));
    } else {
      window.alert(String(result.error));
    }
  };

  return (
    <Wrapper>
      <Body>
        <Title>Google Books 検索</Title>

        <SearchForm>
          <Input placeholder='検索ワードを入力してね！' onChange={event => changeSearchString(event.target.value)} />
          <SearchButton onClick={event => handleOnSearchButton(event)} disabled={!searchString}>
            検索
          </SearchButton>
        </SearchForm>
        {volumeList.kind && <SearchResult volumeList={volumeList} />}
      </Body>
    </Wrapper>
  );
};
```
- 今回のようなシンプルな場合はReduxの恩恵をほとんど感じませんが、コンポーネント内の記述が複雑になってくるとロジックをReduxに分離したことが活きてくるようになります
- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/2361efba8f9a1f6b73a861e1edd3a2c52d803a68


## :pencil: 非同期処理をredux-sagaに置き換える
- さらに非同期処理を`redux-saga`を使うとより関心の分離を意識した設計にすることができます
  - 似たライブラリとして `redux-thunk` もありますが、`redux-saga`と比較してネストが深くなってしまったり、Actionをそのまま扱いづらい、テストがし辛いなどの点で`redux-saga`選定しています
- 今回はAPIとの通信処理をredux-sagaに置き換えてみます

```bash:terminal
yarn add redux-saga
```

- まずは`rootSaga`を用意します

```ts:src/sagas/index.ts
import { all } from 'redux-saga/effects';

export const rootSaga = function* root() {
  yield all([]);
};

```

- `configureStore`のmiddlewareに追加してreduxと合わせて使えるようにします

```ts:src/configureStore.ts
import { rootSaga } from 'sagas';

const sagaMiddleware = createSagaMiddleware();

export function configureStore(preloadedState?: State) {
  const middlewares = [routerMiddleware(history), sagaMiddleware, logger];
  const middlewareEnhancer = applyMiddleware(...middlewares);
  const store = createStore(rootReducer(history), preloadedState, middlewareEnhancer);
  sagaMiddleware.run(rootSaga);
  return store;
}
```
- それでは、実際にredux-sagaで非同期処理を書いてみます
- `GoogleBooksSaga` では特定のActionがdispatchされた時に実行する関数を指定しています
  - `takeLatest` は連続で呼ばれた場合などで最新のものだけ実行する時に使います
- `function* getVolumes()` ではActionのpayloadを使って `searchGoogleBooks()` を呼び、成功した場合のみ `GoogleBooksActions.setVolume()` をdispatchするようにしています

```ts:src/sagas/googleBooks.ts
import { put, takeLatest } from 'redux-saga/effects';
import axios from 'axios';

import { GoogleBooksActions } from 'actions/googleBooks';
import { VolumeList } from 'models/Volume';

const searchGoogleBooks = async (searchString: string) => {
  const url = 'https://www.googleapis.com/books/v1/volumes';
  const params = { q: searchString };
  try {
    const response = await axios.get(url, { params });
    return { isSuccess: true, data: response.data, error: null };
  } catch (error) {
    return { isSuccess: false, data: null, error };
  }
};

function* getVolumes(action: ReturnType<typeof GoogleBooksActions.getVolumes>) {
  const searchString = action.payload;
  const result = yield searchGoogleBooks(searchString);
  if (result.isSuccess) {
    yield put(GoogleBooksActions.setVolumes(VolumeList.fromResponse(result.data)));
  } else {
    window.alert(String(result.error));
  }
}

export function* GoogleBooksSaga() {
  yield takeLatest(GoogleBooksActions.getVolumes, getVolumes);
}
```
- 先ほどの`rootSaga`に`GoogleBooksSaga`を追加します

```ts:src/sagas/index.ts
import { all, fork } from 'redux-saga/effects';

import { GoogleBooksSaga } from 'sagas/googleBooks';

export const rootSaga = function* root() {
  yield all([fork(GoogleBooksSaga)]);
};
```

- 最後に、コンポーネントの処理を置き換えます
  - 処理はActionを呼ぶだけになったため、コンポーネントがかなりスッキリしました

```tsx:src/components/GoogleBooks/index.tsx
export const GoogleBooks: React.FC = () => {
  const [searchString, changeSearchString] = useState('');

  const { volumeList } = useSelector((state: State) => ({ volumeList: state.googleBooks.volumeList }));

  const dispatch = useDispatch();

  return (
    <Wrapper>
      <Body>
        <Title>Google Books 検索</Title>

        <SearchForm>
          <Input placeholder='検索ワードを入力してね！' onChange={event => changeSearchString(event.target.value)} />
          <SearchButton
            onClick={event => {
              event.preventDefault();
              dispatch(GoogleBooksActions.getVolumes(searchString));
            }}
            disabled={!searchString}
          >
            検索
          </SearchButton>
        </SearchForm>
        {volumeList.kind && <SearchResult volumeList={volumeList} />}
      </Body>
    </Wrapper>
  );
};
```

- このように、reduxとredux-sagaを使うことでコンポーネントの記述をほぼrenderだけにすることができました
- 関連Commitはこちら
  - https://github.com/mikan3rd/react-tutorial/commit/caa489fbdb116ed503c89b81e663630380477471

## :pencil: API Clientを作成する
- APIとの通信処理は似たような記述になるのでAPI Clientとして処理を分けてみましょう
- まずはベースをApiClientとして作ってみます

```ts:src/apiClient/index.ts
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios';

import { AxiosResponse } from 'types/responses/axios';

export class ApiClient {
  axiosInstance: AxiosInstance;
  constructor(baseURL = '') {
    this.axiosInstance = axios.create({ baseURL });

    this.axiosInstance.interceptors.request.use(
      async (config: AxiosRequestConfig) => {
        return config;
      },
      (err: AxiosError) => {
        return Promise.reject(err);
      },
    );
  }
  async get<T = object>(path: string, params: object = {}): Promise<AxiosResponse<T>> {
    try {
      const result = await this.axiosInstance.get(path, { params });
      return this.createSuccessPromise<T>(result.data);
    } catch (e) {
      return this.createFailurePromise<T>(e);
    }
  }
  async post<T = object>(path: string, params: object = {}): Promise<AxiosResponse<T>> {
    try {
      const result = await this.axiosInstance.post<T>(path, params);
      return this.createSuccessPromise<T>(result.data);
    } catch (e) {
      return this.createFailurePromise<T>(e);
    }
  }
  async put<T = object>(path: string, params: object = {}): Promise<AxiosResponse<T>> {
    try {
      const result = await this.axiosInstance.put<T>(path, params);
      return this.createSuccessPromise<T>(result.data);
    } catch (e) {
      return this.createFailurePromise<T>(e);
    }
  }
  async delete<T = object>(path: string): Promise<AxiosResponse<T>> {
    try {
      const result = await this.axiosInstance.delete(path);
      return this.createSuccessPromise<T>(result.data);
    } catch (e) {
      return this.createFailurePromise<T>(e);
    }
  }
  async patch<T = object>(path: string, params: object = {}): Promise<AxiosResponse<T>> {
    try {
      const result = await this.axiosInstance.patch<T>(path, params);
      return this.createSuccessPromise<T>(result.data);
    } catch (e) {
      return this.createFailurePromise<T>(e);
    }
  }
  private createSuccessPromise<T>(data: T): Promise<AxiosResponse<T>> {
    return Promise.resolve<AxiosResponse<T>>({ data, isSuccess: true });
  }
  private createFailurePromise<T>(error: AxiosError): Promise<AxiosResponse<T>> {
    return Promise.resolve<AxiosResponse<T>>({ error, isSuccess: false });
  }
}

```

- 上記を使用してGoogle Books APIのVolumeを取得するAPI Clientを作ると次のようになります

```ts:src/apiClient/googleBooks.ts
import { ApiClient } from 'apiClient';
import { JSObject } from 'types/Common';

const baseURL = 'https://www.googleapis.com/books/v1';
const apiClient = new ApiClient(baseURL);

const VOLUME_PATH = '/volumes';

export class VolumeApi {
  static get(params: JSObject): Promise<{}> {
    return apiClient.get(VOLUME_PATH, params);
  }
}

```

- 先ほどのsagaを上記を使って置き換えてみましょう

```ts:src/sagas/googleBooks.ts
import { put, takeLatest } from 'redux-saga/effects';

import { GoogleBooksActions } from 'actions/googleBooks';
import { VolumeList } from 'models/Volume';
import { VolumeApi } from 'apiClient/googleBooks';

function* getVolumes(action: ReturnType<typeof GoogleBooksActions.getVolumes>) {
  const searchString = action.payload;
  const params = { q: searchString };
  const response = yield VolumeApi.get(params);
  if (response.isSuccess) {
    yield put(GoogleBooksActions.setVolumes(VolumeList.fromResponse(response.data)));
  }
}

```
- endpointやmethodごとにAPI Clientを作成することが簡単にできるようになりました
- 関連Commit
  - https://github.com/mikan3rd/react-tutorial/commit/8c7db70d5806bc81dfacb361afd83c0c1a0d49da


- スタイルも整えると次のようになります

![localhost_3000_google_books(Laptop with HiDPI screen).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178077/da8db867-3695-afef-739f-be341e53537d.png)

- GitHubのリポジトリはこちら
  - https://github.com/mikan3rd/react-tutorial

# :star: おわりに
- Reactだけでアプリケーションを作成することもできますが、複数人で開発したりコードが肥大化してきた時には上記のように型安全や関心の分離を意識することが重要になってくるはずなので、参考にしてもらえれば幸いです