ReduxとReact （Native）Hooksとの共存

`React`は「自由にカスタマイズできる」かつ「日進月歩」というのがメリットの反面、
ネット上には新旧の情報が入り乱れていてベストプラクティスが見つけづらいというのがデメリットになっているのではないでしょうか。

その最たる例が`Hooks`、`Redux`をどういった構成にするかだと思います。
巷では`Hooks`は`Redux`の機能を補えるから、`Redux`はもはや必要ないといった意見もでているようです。

本当に不必要なのでしょうか？


初学者の方にもなるべくわかりやすいように`Redux`の概要・非同期処理について根本的なところから改めて振り返って整理したのち、`Hooks（useReducer・useContext）`との設計概念・機能比較を行いました。
「もうそんなこと知ってるよ」という方は、`Redux`概要の箇所は飛ばしていただけるとありがたいです。

## <font color="LightSeaGreen">【Redux概要】 MVC・Flux・Reduxアーキテクチャ比較</font>
`Redux`自体 `MVC → Flux → Redux` と既存のアーキテクチャの欠点を補うために生まれてきたアーキテクチャのため、遠回りなように思えますが、理解を深めるためにはこれらの差異について知ることが重要になってきます。

### MVCアーキテクチャ
アプリケーション開発の学習を始めると一番最初に覚えるアーキテクチャではないでしょうか？ 初心者にもシンプルでわかりやすいです。私自身もプラグラミングスクールで`Ruby on Rails`を使い開発を行っていたのでお馴染みのアーキテクチャです。

単純なアーキテクチャであればシンプルに以下のようなフローになります。
従来型のフルサーバサイドレンダリングアプリケーションであれば、`Model・View`間のデータフローは単方向ですが、`React`などの`SPA`アプリ開発においては、`Model・View`双方向にデータの受け渡しがあるところがポイントになります。
![MVC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/b5f1e3f7-1b3c-a859-84c8-d5473efc79ed.png)
複雑なアーキテクチャになればなるほど、`Model・View`間のフローのパターンは指数関数的に増大してしまいメンテナンスが難しくなってしまいます。（以下図は概念を理解するために若干大げさな表記になっています。）
![MVC_complex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/59315fa7-89bc-ac06-6602-ee8d543ef8c6.png)

### Fluxアーキテクチャ

`Flux`アーキテクチャは上記`MVC`モデルの煩雑なデータフローを解消するために生まれました。
つまり、シンプルなアプリであれば`MVC`モデルでもなんら問題はないということになりますね。

`MVC`にたとえるなら`Dispatcher`が`Controller`で`Store`が`Model`といったところです。
ただし以下の点で異なっています。

- `Store`は一枚岩で複数のオブジェクトの状態を管理している（`Model`のようにオブジェクトごとに個別に存在しない）
- `Store`の変更は、`Action`から派生する`Dispatcher`によってのみ行われる。`View`から直接操作することはない。
- `Store`の値が変更されると、直ちに`View`に反映される。

上記のようなデータフローにより、`Store・View`はどれだけアプリケーションのボリュームが大きくなっても、単方向のデータフローに集約することができるようになります。つまり「どこの`Model`がどこの`View`を更新して〜」、「どこの`View`がどこの`Model`を更新して〜」、などを考えなくても単純に「`View`から『何をしたいか』の`Action`を発行し、その変更を`Store`が受けつけ、`Store`の状態が即時に`View`に反映される」といったシンプルなデータフローになります。

![Flux.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/6019a856-464e-55b8-583b-a4386fa49adb.png)
### Reduxアーキテクチャ
`Redux`は`Flux`アーキテクチャに更に制限を追加して、アプリケーションの状態管理をスムーズにするフレームワークです。
`Flux`のデータフローに加えて以下のような制限が加わります。

- `Store`はアプリケーションに1つのみ存在する。つまり状態の更新先・参照先は常に同じ`Store`（シングルトン）
- `Store`の状態（`State`）を変更できるのは、`Reducer`のみ
- `Reducer`は`Store`内の`State`を、`View`より発行された「一意の`ActionType`」により振り分け、新しい`State`に更新する
- `Reducer`は常に同じ結果を返す（非同期処理やランダムな計算を行ってはいけない）

![Redux.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/c538549c-4361-9d11-86ca-ad5ea3fefdfa.png)


## <font color="LightSeaGreen">【Redux概要】 Reduxで非同期処理を扱う</font>

本格的なアプリケーション開発において、「非同期処理（外部APIとの連携）」を行わないケースは少ないと思います。
そのため非同期処理の実装は重要になってくるのですが、`Redux`においても`Middleware`を導入することで、非同期処理を実現することができます。
今回は`Middleware`の中でも人気の`Redux-thunk`と`Redux-saga`について、特徴を整理してみます。

### Redux Thunk
非常にシンプルな`Middleware`で、`Thunk`を導入すると`ActionCreater`内部のロジックで非同期処理が実行できるようになります。
`React`同様、いい意味でも、悪い意味でもコード設計は自由なため、構成をうまく考えないとファット`Action`になってしまうリスクもあります。
![ReduxThunk.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/51ce74d5-82ff-8339-8324-f855926cc706.png)


### Redux Saga
`Redux-saga`については、非同期処理は`Saga`に完全に切り分けて`Redux`とは別で処理を回すという考え方なので、ファイル数は多くなりますが`redux-thunk`よりこちらのほうが各機能の役割がはっきりして処理の流れが掴みやすくなります。
ただし、`Saga`の内部処理ではあまり他では見かけないジェネレータ関数を利用しているため、`Thunk`よりも学習コストがかかるというデメリットがあります。

![ReduxSaga.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/d22aed5a-827f-29dc-1805-0a1e41662f90.png)

## <font color="LightSeaGreen">Hooksが導入されてできるようになったこと</font>

### useReducer
`React`のみでも、「`Action`を発行 → `Reducer`内部で`ActionType`により`State`の値を更新 → `View`の表示が変更」といった`Redux`のような外部フレームワークでしか実現できなかった`Flux`ベースのアーキテクチャが組めるようになりました。
ただし、`Store`の概念はなく、状態の管理は各オブジェクトごとに分散して管理する必要があります。（`Redux`っぽく、すべてのオブジェクトの`Reducer`を`Combine`できなくはないようですが、それなら`redux`を使ったほうが効率的です。）

### useContext
コンポーネントのトップレベルで定義した`state`をその配下のコンポーネント（子・孫・ひ孫・・・）から、`props`でのバケツリレーなしで値を取得することができます。

### useReducer と useContextを組み合わせてReduxフローを実現
`useReducer`と`useContext`を組み合わせることにより、`Redux`のフローを再現してみました。
下記コードは親と子の単純な例ですが、もっとコンポーネントの階層が深くなった場合でもRootコンポーネントで定義した`State`にアクセス・`Action`を`Dispatch`することが可能になり、`Redux`よりも簡単かつ軽量にデータフローを実現することができます。

```typescript:App.tsx(root)
import React, {
  FC,
  useReducer,
  createContext,
} from 'react';
import AppScreen from './AppScreen';

const initialState = { count: 0 };

interface StateProps {
  count: number;
}

interface ActionProps {
  type: string;
}

const reducer = (state: StateProps, action: ActionProps) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};

interface StoreContextProps {
  state: StateProps;
  dispatch: ({ type }: ActionProps) => void;
}

export const StoreContext = createContext({} as StoreContextProps);

const App: FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      <AppScreen />
    </StoreContext.Provider>
  );
};

export default App;
```

```typescript:AppScreen.tsx
import React, { FC, useContext } from 'react';
import { StoreContext } from './App';

const AppScreen: FC = () => {
  const { state, dispatch } = useContext(StoreContext);

  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
};

export default AppScreen;

```
![useReducer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/e27f4907-d4df-d289-a826-eb5db1bd7baf.png)

### useReducer で非同期処理をする
`useReducer`を使ったデータフローでは`Middleware`の導入を前提としていないようですが、`Middleware`なしでも非同期処理は行うことができます。
色々と調べていたのですが、`Redux`のようにアーキテクチャのベストプラクティスのようなものは定まっていないように感じました。結局`Redux`のようなアーキテクチャにするなら`Redux`を最初から導入すればいいし、違ったやり方をするのであればグローバルの状態管理項目が増えた場合にカオスになりそうな気がするし、`React`の放任主義はいつものことですが、ここも悩ましいところですね...。

```typescript:App.tsx(root)
import React, {
  FC,
  useReducer,
  createContext,
} from 'react';
import axios, { AxiosError, AxiosResponse } from 'axios';
import AppScreen from './AppScreen';

axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://xxxxxxxxxxxxxxxx.com/api';

interface StateProps {
  result: AxiosResponse;
  isLoading: boolean;
  error?: AxiosError | null;
}

interface ActionProps {
  type: string;
  payload?: any;
  error?: boolean;
}

const initialState: StateProps = {
  result: {} as AxiosResponse,
  isLoading: false,
};

const reducer = (state: StateProps = initialState, action: ActionProps) => {
  switch (action.type) {
    case 'start':
      return {
        ...state,
        isLoading: true,
      };
    case 'succeed':
      return {
        ...state,
        result: action.payload?.result,
        isLoading: false,
      };
    case 'error':
      return {
        ...state,
        result: action.payload?.error,
        isLoading: false,
      };
    default:
      throw new Error();
  }
};

interface StoreContextProps {
  state: StateProps;
  dispatch: ({ type }: ActionProps) => void;
}

export const StoreContext = createContext({} as StoreContextProps);

const App: FC = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <StoreContext.Provider value={{ state, dispatch }}>
      <AppScreen />
    </StoreContext.Provider>
  );
};

export default App;

```

```typescript:AppScreen.tsx
import React, { FC, useContext } from 'react';
import axios from 'axios';
import { StoreContext } from './App';

const AppScreen: FC = () => {
  const { state, dispatch } = useContext(StoreContext);

  const clickHandler = async () => {
    dispatch({ type: 'start' });
    try {
      const result = await axios.get('/users');
      if (result.status !== 200) {
        throw new Error('The request has failed');
      }
      dispatch({ type: 'succeed', payload: { result }, error: true });
    } catch (error) {
      dispatch({ type: 'error', payload: { error }, error: true });
    }
  }

  return (
    <>
      Result: {JSON.stringify(state.result)}
      <button onClick={clickHandler}>データ取得</button>
    </>
  );
};

export default AppScreen;
```


## <font color="LightSeaGreen">Hooksの機能でもReduxと同じようなことはできる。それでもReduxを使うメリット</font>

### Reduxの設計原則は素晴らしい
ルールがありすぎるのは時には不自由に感じますが、コードが複雑になればなるほどその恩恵を受けることができます。その点、`Redux`にはアーキテクチャに関するドキュメントがしっかりとまとまっており、この原則にある程度従っておけばそこまで的はずれな構成にならないというのは大きいです。
`Hooks`を使うから、`Redux`の原則よりも素晴らしいアーキテクチャが組めるようになるというなら話は別ですが、`Hooks`を使っても結局`Redux`のアーキテクチャを模倣してコードを組むのであれば最初から`Redux`を使っておけば良いという見解です、
逆に原則やルールが必要がないほど、シンプルなアプリケーションであるならば`Redux`を導入する意味はあまりないので、`Hooks（useReducer・useContext）`を使ってサクッと作るのもありかと思います。

### 異なるコンポーネント間でのやりとりが容易になる

原則部分とかぶる部分もありますが、グローバルの状態を`Store`が一手に引き受け、かつアプリ上のすべての`Reducer`をコンバインしてどのコンポーネントからでも「状態の取得」、「アクションを通しての状態の変更」ができるのは便利です。
`useReducer`には`Store`の概念がそもそもありません。`Hooks`を使ってもRootコンポーネントで`Context`を定義すればできないことはないですが、もともとそういったアーキテクチャ用の`Hook`ではないので開発者独自の記述方法になって後々メンテナンスに苦労するということになりかねません。
`Store`の概念を使いたいなら、おとなしく`Redux`を導入して、`Redux`で管理するまでもないところのみ`useReducer`で処理するというのが現状のベストではないでしょうか。

### `Middleware`により非同期処理と状態管理を分離できる
非同期通信を行うための`Middleware`を自由に選べるのもメリットの一つです。`Redux-thunk`は自由度が高く、技術者の裁量に任されてしまいますが、`Redux-saga`を使用した場合、外部APIとの非同期処理は`saga`にかき分けることを強制されるので、状態管理と非同期処理を明確に分離することができます。これによりテストが書きやすく、保守性の高いプロダクトを保持することができます。

### `Middleware`によりアクション発行履歴（トランザクション）を管理し、巻き戻しできる
これもまた`Middleware`の話になりますが、`Redux devtools`を導入することにより、グローバルStateの可視化、アクション発行履歴の管理、タイムトラベルデバッギングができるようになり、開発効率を高めることができます。
![e23206f8dbc0ff1acb9f2ff6ed01118a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/27db9e8e-e6c4-a6d6-ac82-ad6a032d80a7.gif)

## <font color="LightSeaGreen">Redux with Hooks</font>
`Redux`自体も`v7.1.0`以降`Hooks`の恩恵を受け随分とシンプルにかけるようになりました。
また、初期導入時には`Redux Tool Kit`でコマンド一発で`Redux & Hooks & Typescript`の開発環境が整うようです。
もっと早く欲しかった..。

### useSelector & useDispatch
`mapStateToProps`と`mapDispatchToProps`をコンポーネントに`connect`せずに`Redux`の`Store`にアクセスできるようになったので、記述量が減るだけでなくかなりフレキシブルに`Redux`を扱うことができます。

```typescript:従来までの書き方
import React, { FC, useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';

import { fetchAllItems, deleteItem } from '../actions/item';

const mapStateToProps = (state: AllState) => ({
  items: state.item.items,
  isLoading: state.item.isLoading,
});

const mapDispatchToProps = (dispatch: Dispatch) =>
  bindActionCreators(
    {
      fetchItemsStart: () => fetchAllItems.start(),
      deleteItemStart: (itemId: ItemId) => deleteItem.start(itemId),
    },
    dispatch,
  );

interface HomeScreenProps extends ScreenNavigationProp {
  items: ItemModel[];
  isLoading: boolean;
  fetchItemsStart: () => void;
  deleteItemStart: (itemId: ItemId) => { payload: { id: number } };
}

const HomeScreen: FC<HomeScreenProps> = ({
  items,
  isLoading,
  fetchItemsStart,
  deleteItemStart,
  navigation,
  route,
}) => {
  useEffect(() => {
    (async () => {
      await fetchItemsStart();
    })();
  }, []);

  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      (async () => {
        await fetchItemsStart();
      })();
    });

    return unsubscribe;
  }, [navigation]);

  const deleteItem = async (itemId: number) => {
    alert(`deleted ${itemId}`);
    await deleteItemStart({ id: itemId });
  };

  return (
    ・・・
  );
};

export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
```

```typescript:HooksAPIを使った書き方
import React, { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { fetchAllItems, deleteItem } from '../actions/item';

const HomeScreen: FC<ScreenNavigationProp> = ({ navigation, route }) => {
  const dispatch = useDispatch();
  const itemState = useSelector((state: AllState) => state.item);

  useEffect(() => {
    (async () => {
      await dispatch(fetchAllItems.start());
    })();
  }, []);

  useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      (async () => {
        await dispatch(fetchAllItems.start());
      })();
    });

    return unsubscribe;
  }, [navigation]);

  const deleteButtonHandler = async (itemId: number) => {
    alert(`deleted ${itemId}`);
    await dispatch(deleteItem.start({ id: itemId }));
  };

  return (
    ・・・
  );
};

export default HomeScreen;

```

### Redux Tool Kit

私自身使ったことがないため導入方法リンクのみ共有しておきます。
> https://redux-toolkit.js.org/introduction/quick-start

## <font color="LightSeaGreen">まとめ</font>
- **Hooksの登場により、Reduxは唯一の選択肢ではなくなった**
- **複雑かつ大規模なプロダクトにはReduxの設計原則が最も力を発揮する**
- **Hooks useReducer + useContextはReduxと競合関係にあるわけではなく共存・使い分けできる**
- **Reduxの設計原則が必要ないほどシンプルなプロダクトならばHooksのみでグローバルの状態も管理できる**
- **Reduxの導入ハードルは年々下がってきている**