redux, redux-sagaが少しだけ理解できたので，日報アプリを自作してみた

# 本記事の目的

こんにちは！@flat-fieldです！
普段はアルバイトでバックエンドの実装を行なっておりますが，フロントエンドも少しは触れるようになりたいという思いから，react, redux, redux-sagaの勉強をしていました．
最初は「難しいな〜〜」と思うことばかりでしたが,3週間ほど勉強する中で少しずつ理解できることも増えたので，これらの技術を用いて日報アプリを作ってみました．

本記事では，**redux**, **redux-saga**の仕組みについて，自作した日報アプリを元に説明できればと思います．
なお，細かい実装の部分には触れていません．なんとなく「redux, redux-sagaはこんな感じなんだなあ」と分かっていただければ幸いです(実装について詳しく理解したい方は，おすすめの書籍を最後に紹介しているので，そちらを参照してみてください)．
なお勉強して間もないので，何か間違っていることや，補足事項がありましたら，コメントで教えていただけると有り難いです．

よろしくお願いいたします．

# reduxとは？

Reactでは，コンポーネントを組み合わせてアプリケーションを構成します．
コンポーネントには「状態(state)を持つコンポーネント」と「状態(state)を持たないコンポーネント」があります．
**実際のアプリケーションでは，複数のコンポーネント間で共有したいstateが存在することがよくあります．**
例えば，ユーザのログイン情報を複数のコンポーネントで利用したいときなどが挙げられます．

では，あるstateを複数のコンポーネントで使用するにはどうすればいいでしょうか？

安直な方法としては，最上位コンポーネントに全てのstateを集約し，それを子コンポーネントにバケツリレーのごとくstateを引き継いであげる，ということが考えられます．でも，流石にそんなことしたくないですよね...

そこで，reduxが登場します．reduxは以下のような3つの原則を採用しており，シンプルかつ簡潔にあるstateを複数のコンポーネントで使用することができるようになります．

- **Single source of truth** 
- **State is read-only**
- **Changes are made with pure functions**

"Single source of truth"は，アプリケーションの全てのstateを"store"と呼ばれるオブジェクトで一元管理することを意味します．
複数のstoreで管理すると，store間のデータのやりとりが複雑になり，バグの温床となります．1つのstoreで管理することによって，テストやデバッグをやりやすくすることができます．

"State is read-only"は，stateを直接書き換えることができないことを意味します．もしstateを変更したい場合は，必ず"action"を発行する必要があります．この"action"を必ず発行しないとstateの状態を変更することができない仕組みによって，予期せぬ場所からstateの書き換えができないようにしています．

"Changes are made with pure functions"は，stateの変更は"Reducer"と呼ばれる純粋関数(引数が同じならば，必ず同じ結果が返される関数)によって行われます．つまりReducerは，stateとactionの2つを引数とし，新たなstateを返す，ということを実行します．

```
# 言うなればReducerはこんな感じ
(prevState, action) => newState
```

reduxの全体の概念図は以下の通りです．

![スライド1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376174/ad14f51a-b6ff-013a-a089-e5f5c6251481.jpeg)

図を見てもらうとわかるのですが，**各ノードの依存関係が単方向ですね**．これによりアプリケーションの構造が複雑になるのを防いでいます．

# redux-sagaとは？

redux-sagaとは，一言で言うと**「reduxで非同期処理を扱うための仕組み」**のことです．
全体の概念図を見てみましょう．

![スライド1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376174/b2bb5ce9-28d0-4b82-4fb5-ebb7ca89a509.jpeg)

ややこしくなったように見えますが，実は右側2/3はreduxと同じ構造になっています．reduxにsagaという，actionを監視して非同期処理を実現する機能を追加したものがredux-sagaです．

**このように，特定のactionをWatch Taskが監視し，Taskが実際の非同期処理を実現することにより，シンプルな構造のまま非同期処理を実現することができます．**

ちなみに余談ですが，reduxで非同期処理を実現するもう一つの手法として「redux-thunk」というものがあります．こちらはredux-sagaに比べて学習コストが小さくて済むというメリットがありますが，依存関係が複雑になり，react本来のコンポーネントの振る舞いができなくなるというデメリットがあります．

# 日報アプリを作ってみた！

redux, redux-sagaについて説明したところで，実際に日報アプリを作ってみました．
この日報アプリでは，以下のことができます．

```
- その日の振り返り，明日の目標を毎日記録することができる
- 当日の日報のみ修正することができる．
- 過去の日報を閲覧することができる．
```

Githubは[こちら](https://github.com/fieldflat/daily-system)から参照してください．
(なお，APIについても自作しており，[こちら](https://github.com/fieldflat/daily-system-backend)から参照できます．)

ちなみにこんな感じの動作です⬇️．
![ezgif-5-1e8a54e3bf09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376174/bcb29b51-c255-7a94-6ce2-bffd6086bf38.gif)


## redux-sagaを用いた開発の流れ
全体の概念図の流れ通りに実装すると良さそうです．なので以下のような流れで実装しました．
(1) まずは非stateコンポーネント(Viewの役割をメインに担う)を作成．
(2) Actionを実装する．
(3) Reducerを実装する．
(4) 非同期処理通信を実装する．
(5) (4)の非同期処理を利用するSagaのタスクを作成する．
(6) 最上位のコンポーネントに，全コンポーネントでsagaを利用できるような仕組みを実装する．
(7) 最後に，state有コンポーネント((1)をimportして，機能を追加するコンポーネント)を作成する．

### (1) まずは非stateコンポーネント(Viewの役割をメインに担う)を作成．
序盤でもお伝えしましたが，コンポーネントには2種類有ります．ここでは，非stateコンポーネントを先に実装し，Viewを整えます．

`src/components/*.tsx`がこれに該当します．

一例として，日報を記入するためのコンポーネント`src/components/WhiteDailyPart.tsx`をみてみましょう．

```typescript:src/component/WhiteDailyPart.tsx
import React, { FC } from 'react';
import { Grid, Form, Button } from 'semantic-ui-react';

export interface WriteDailyProps {
  postDailyTmp?: () => void;
}

const WriteDailyPart: FC<WriteDailyProps> = ({
  postDailyTmp = () => undefined,
}) => (
  <Grid.Column width={10}>
    <Form>
      <Form.Field>
        <Form.TextArea
          id="today"
          label="今日の振り返り"
          rows={20}
          placeholder="ここに本日の振り返りを記入してください．フォーマットは規定のものにしたがってください．そして毎日継続しまししょう．"
        />
      </Form.Field>

      <Form.Field>
        <Form.TextArea
          id="tomorrow"
          label="明日の目標"
          rows={10}
          placeholder="明日の目標を具体的に記述しましょう．"
        />
      </Form.Field>

      <Form.Field>
        <Form.Input
          fluid
          id="point"
          label="本日の点数(100点満点中)"
          placeholder="本日の点数(100点満点中)"
        />
      </Form.Field>

      <Button type="submit" onClick={postDailyTmp}>
        提出する
      </Button>
    </Form>
  </Grid.Column>
);

export default WriteDailyPart;
```

### (2) Actionを実装する．
続いてActionを実装します．`src/actions/daily.ts`をみてみましょう．

流れとしては，

- Action Type(それぞれのActionに付与するもの)を定義
- Actionを発行するための関数を実装
- Return Typeをまとめて型定義

という感じです．

```typescript:src/actions/daily.ts
import { AxiosError } from 'axios';
import { Daily } from '../services/daily/model';

// 1. Action Typeを定義
export const REGISTER_DAILY_START = 'DAILY/REGISTER_DAILY_START';
export const REGISTER_DAILY_SUCCEED = 'DAILY/REGISTER_DAILY_SUCCEED';
export const REGISTER_DAILY_FAIL = 'DAILY/REGISTER_DAILY_FAIL';

export const GET_DAILIES_START = 'DAILY/GET_DAILIES_START';
export const GET_DAILIES_SUCCEED = 'DAILY/GET_DAILIES_SUCCEED';
export const GET_DAILIES_FAIL = 'DAILY/GET_DAILIES_FAIL';

// 2. Actionの発行
export const registerDaily = {
  start: (params: Daily) => ({
    type: REGISTER_DAILY_START as typeof REGISTER_DAILY_START,
    payload: params,
  }),

  succeed: (params: Daily, result: Daily) => ({
    type: REGISTER_DAILY_SUCCEED as typeof REGISTER_DAILY_SUCCEED,
    payload: { params, result },
  }),

  fail: (params: Daily, error: AxiosError) => ({
    type: REGISTER_DAILY_FAIL as typeof REGISTER_DAILY_FAIL,
    payload: { params, error },
    error: true,
  }),
};

export const getDailies = {
  start: () => ({
    type: GET_DAILIES_START as typeof GET_DAILIES_START,
  }),

  succeed: (result: Daily[]) => ({
    type: GET_DAILIES_SUCCEED as typeof GET_DAILIES_SUCCEED,
    payload: { result },
  }),

  fail: (error: AxiosError) => ({
    type: GET_DAILIES_FAIL as typeof GET_DAILIES_FAIL,
    payload: { error },
  }),
};

// 3. ReturnTypeをまとめて型定義．あとで使う．
export type DailyAction =
  | ReturnType<typeof registerDaily.start>
  | ReturnType<typeof registerDaily.succeed>
  | ReturnType<typeof registerDaily.fail>;

export type GetDailiesAction =
  | ReturnType<typeof getDailies.start>
  | ReturnType<typeof getDailies.succeed>
  | ReturnType<typeof getDailies.fail>;
```


### (3) Reducerを実装する．
続いてReducerを実装します．前述の通り，Reducerはactionを受け取って，それぞれのactionごとに`(prevState, action) => newState`を実行します．

```typescript:src/reducer.ts
import { Reducer } from 'redux';

import { Daily } from './services/daily/model';
import {
  DailyAction,
  GetDailiesAction,
  REGISTER_DAILY_START,
  REGISTER_DAILY_SUCCEED,
  REGISTER_DAILY_FAIL,
  GET_DAILIES_START,
  GET_DAILIES_FAIL,
  GET_DAILIES_SUCCEED,
} from './actions/daily';

export interface DailyState {
  dailies: Daily[];
}

export const initialState: DailyState = { dailies: [] };

const counterReducer: Reducer<DailyState, DailyAction | GetDailiesAction> = (
  state: DailyState = initialState,
  action: DailyAction | GetDailiesAction,
): DailyState => {
  switch (action.type) {
    case REGISTER_DAILY_START:
      return {
        ...state,
        dailies: state.dailies,
      };
    case REGISTER_DAILY_SUCCEED:
      return {
        ...state,
        dailies: [action.payload.result, ...state.dailies],
      };
    case REGISTER_DAILY_FAIL:
      return {
        ...state,
      };
    case GET_DAILIES_START:
      return {
        ...state,
        dailies: state.dailies,
      };
    case GET_DAILIES_SUCCEED:
      return {
        ...state,
        dailies: [...state.dailies, ...action.payload.result],
      };
    case GET_DAILIES_FAIL:
      return {
        ...state,
      };
    default: {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      // const _: never = action;

      return state;
    }
  }
};

export default counterReducer;
```

### (4) 非同期処理通信を実装する．
続いて非同期処理通信を実装します．
`src/services/daily/api.ts`では，APIを叩くためのインタフェース(postDailyFactory, getDailiesFactory)を提供しています．

```typescript:src/services/daily/api.ts
import axios from 'axios';

import { Daily } from './model';

interface ApiConfig {
  baseURL: string;
  timeout: number;
}

const DEFAULT_API_CONFIG: ApiConfig = {
  baseURL: 'http://localhost:8080',
  timeout: 7000,
};

// クロージャーの利用
// 実際の使い方：const getMembers = getMembersFactory({timeout: 3000});
//              try {
//                const users = await getMembers(‘facebook’);
//              } catch(err) {
//                console.log(err);
//              }

export const postDailyFactory = (optionConfig?: ApiConfig) => {
  const config = {
    ...DEFAULT_API_CONFIG,
    ...optionConfig,
  };
  const instance = axios.create(config);

  const postDaily = async (daily: Daily) => {
    const params = new URLSearchParams();
    params.append('id', `${daily.id}`);
    params.append('date', daily.date);
    params.append('today', daily.today);
    params.append('tomorrow', daily.tomorrow);
    params.append('point', daily.point);
    const response = await instance.post(`/daily/create`, params);

    if (response.status !== 200) {
      throw new Error('Server Error');
    }
    const responseDaily: Daily = {
      id: response.data.daily.ID,
      date: response.data.daily.date,
      today: response.data.daily.today,
      tomorrow: response.data.daily.tomorrow,
      point: response.data.daily.point,
    };

    return responseDaily;
  };

  return postDaily;
};

export const getDailiesFactory = (optionConfig?: ApiConfig) => {
  const config = {
    ...DEFAULT_API_CONFIG,
    ...optionConfig,
  };
  const instance = axios.create(config);
  const dailies: Daily[] = [];

  const getDailies = async () => {
    const response = await instance.get(`/dailies`);

    if (response.status !== 200) {
      throw new Error('Server Error');
    }
    // console.log(response.data.dailies);
    for (let i = 0; i < response.data.dailies.length; i += 1) {
      const d: Daily = {
        id: response.data.dailies[i].ID,
        date: response.data.dailies[i].date,
        today: response.data.dailies[i].today,
        tomorrow: response.data.dailies[i].tomorrow,
        point: response.data.dailies[i].point,
      };
      dailies.push(d);
    }

    // return JSON.stringify(response.data.dailies);
    return dailies;
  };

  return getDailies;
};
```

### (5) (4)の非同期処理を利用するSagaのタスクを作成する．
続いて，sagaのタスクを作成します(src/sagas/)．この部分では，特定のactionを観測したら，(4)で作成したAPIインタフェースを利用してAPIを叩き，正しく結果が返ってきたらsucceedアクションを発行し，stateを変更します．もしエラーが返ってきたらfailアクションを発行します．

```typescript:daily.ts
import { all, call, fork, put, takeLatest } from 'redux-saga/effects';

// import * as Action from '../actions/daily';
import {
  registerDaily,
  getDailies,
  REGISTER_DAILY_START,
  GET_DAILIES_START,
} from '../actions/daily';
import { postDailyFactory, getDailiesFactory } from '../services/daily/api';

function* runPostDaily(action: ReturnType<typeof registerDaily.start>) {
  const daily = action.payload;
  try {
    const api = postDailyFactory();
    const resultDaily = yield call(api, daily);

    yield put(registerDaily.succeed(daily, resultDaily));
  } catch (error) {
    yield put(registerDaily.fail(daily, error));
  }
}

function* runGetDailies(action: ReturnType<typeof getDailies.start>) {
  try {
    const api = getDailiesFactory();
    const resultDailies = yield call(api);

    yield put(getDailies.succeed(resultDailies));
  } catch (error) {
    yield put(getDailies.fail(error));
  }
}

// take … 特定のActionを待ち受ける．
// takeEvery … 渡されたActionの数だけ律儀にタスクを実行する
// takeLatest … 最新のものだけを実行する
export function* watchPostDaily() {
  yield takeLatest(REGISTER_DAILY_START, runPostDaily);
}

export function* watchGetDailies() {
  yield takeLatest(GET_DAILIES_START, runGetDailies);
}

// これをSagaミドルウェアに渡すとアプリ起動時に同時に起動されて，ここでforkされた分だけ別のタスクも立ち上がってスタンバイする．
export default function* rootSaga() {
  yield all([fork(watchPostDaily), fork(watchGetDailies)]);
}
```

### (6) 最上位のコンポーネントに，全コンポーネントでsagaを利用できるような仕組みを実装する．
最上位コンポーネントである，`src/index.tsx`にsagaを利用できる仕組みを実装します．

```typescript:src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { BrowserRouter } from 'react-router-dom';
import createSagaMiddleware from 'redux-saga';

import App from './App';
import dailyReducer from './reducer';
import rootSaga from './sagas/daily';
import * as serviceWorker from './serviceWorker';

import './index.css';
import './styles/semantic.min.css';

// 以下2行を追加
const sagaMiddleWare = createSagaMiddleware();
const store = createStore(dailyReducer, applyMiddleware(sagaMiddleWare));

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root') as HTMLInputElement,
);

serviceWorker.unregister();

// これも追加
sagaMiddleWare.run(rootSaga);
```

### (7) 最後に，state有コンポーネント((1)をimportして，機能を追加するコンポーネント)を作成する．
最後に，state有コンポーネント(通称コンテナ)を作成して，storeとコンポーネントのpropsを結びつける作業をします．
`src/containers/*.tsx`が該当のコンテナです．ここでは`src/containers/WriteDailyPart.tsx`をみていきましょう．

```typescript:src/containers/WriteDailyPart.tsx
import React, { FC } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { RouteComponentProps, withRouter } from 'react-router';

import WriteDailyPart, { WriteDailyProps } from '../components/WriteDailyPart';
import { Daily } from '../services/daily/model';
import { DailyState } from '../reducer';
import { registerDaily } from '../actions/daily';

interface StateProps {
  dailies: Daily[];
}

interface DispatchProps {
  postDaily: (daily: Daily) => void;
}

type EnhancedMembersProps = WriteDailyProps &
  StateProps &
  DispatchProps &
  RouteComponentProps;

const mapStateToProps = (state: DailyState): StateProps => ({
  dailies: state.dailies,
});

const mapDispatchToProps = (dispatch: Dispatch): DispatchProps =>
  bindActionCreators(
    {
      postDaily: (daily: Daily) => registerDaily.start(daily),
    },
    dispatch,
  );

const DailyContainer: FC<EnhancedMembersProps> = ({ dailies, postDaily }) => {
  const postDailyTmp = () => {
    const today = document.getElementById('today') as HTMLInputElement;
    const tomorrow = document.getElementById('tomorrow') as HTMLInputElement;
    const point = document.getElementById('point') as HTMLInputElement;
    const todayDate = new Date();
    const date =
      `${todayDate.getFullYear()}` +
      `${('0' + (todayDate.getMonth() + 1)).slice(-2)}` +
      `${todayDate.getDate()}`;
    const daily: Daily = {
      id: 3,
      date,
      today: today.value,
      tomorrow: tomorrow.value,
      point: point.value,
    };
    postDaily(daily);
  };

  return <WriteDailyPart postDailyTmp={postDailyTmp} />;
};

// DailyContainerの(state, dispatch)と，storeやactionで管理している(state, dispatch)を結びつける.
export default withRouter(
  connect(mapStateToProps, mapDispatchToProps)(DailyContainer),
);
```

これで完成です．

# 最後に
redux-sagaは概念自体はそこまで難しくないのですが，実装するのはかなり難しかったです.
私は[この本](https://booth.pm/ja/items/1312652)で学習することにより，体系的にreact, redux, redux-sagaを学ぶことができました．
おすすめなので，この記事でreact, redux, redux-sagaに興味を持った方は，一度ご購入を検討してみてください！