【React】React+ReduxのプロジェクトにTypeScriptを導入してみた

#はじめに
これまでTypeScriptの基礎やReactプロジェクトにTypeScriptを導入する方法について学習してきたのですが、ReduxへのTypeScriptの導入については現場でReduxを使っていないこともありタッチしていませんでした。
しかし、今後Redux+TypeScriptの案件に携わる可能性がないともいいきれないので、この機に学んでみることにしました。

https://qiita.com/suzuki0430/items/86a5b4c3c33c48c8bc44

https://qiita.com/suzuki0430/items/f8d5cbbe9401c5945098

#プロジェクト設計
NPMのAPIでパッケージ名を検索する簡単なプロジェクトをReact+Redux+TypeScriptで作成しました。
![スクリーンショット 2021-05-20 22.08.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/fe667158-2ee3-1db5-2df8-73f68240250b.png)

##Store設計
Reduxのプロジェクトを作成する際に一番最初にすべきことがStoreの設計です。
これはTypeScriptを導入する際にはさらに重要なステップとなります。

今回のケースでは、repositoriesというReducerをつくり、data(検索結果のデータ)、loading(ロード状態の判定)、error(エラーメッセージ)のプロパティをもったオブジェクトがstateとして返ってくるような構成となります。
このとき、packageがTypeScriptでは予約語にあたるので、packagesのことをrepositoriesとしています。
![スクリーンショット 2021-05-21 7.11.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/5c76e9c0-76ab-49b7-e84b-1d6905a16810.png)

##Redux設計
Storeの設計ができたら、次にAction CreatorやActionを加えた全体の設計を考えます。

今回はAction Creator1つに対して3つのActionを用意します。
SearchRepositoriesはロード、SearchRepositoriesSuccessは検索結果、SearchRepositoriesErrorはエラーを返したいときにDispatchするActionとなります。
![スクリーンショット 2021-05-21 7.52.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/3b33fcd4-1068-4b77-75ee-c1573bb334c6.png)

##React/Redux連携
ReduxではReducer、Action Creator、Middlewareが必要になるため、それぞれのフォルダやファイルがつくられます。
ReactとReduxを連携する際、Reactのコンポーネントでこれらのファイルをインポートすることになるのですが、インポート先がバラバラなため若干複雑な構成になってしまいます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/0aeb963f-a70f-eca4-d0d1-8ad258901660.png" width="700" />

ここでRedux側に`index.ts`というエンドポイントをつくり、ここからReduxの各フォルダにアクセスできるようにします。
こうすることで、ReactとReduxをシンプルに連携することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c6872f7b-f22b-ea6a-b700-0158ff16f9a7.png" width="700" />

#実装
プロジェクト設計が終わったら実装に入ります。
Redux側とReact側で分けて説明します。

##Reduxの実装
Reducer, Action Creator, エンドポイントの作成について主に説明します。

###Reducerの作成
SearchRepositories, SearchRepositoriesSuccess, SearchRepositoriesErrorの各Actionに対して、ロード中、検索結果のデータ、エラーの状態(State)を返すようなReducerを作成します。
例えば、ロード中であれば`{ loading: true, error: null, data: [] }`というStateを返し、検索が成功したら`{ loading: false, error: null, data: action.payload }`のようにデータの入ったStateを返すようにします。

```tsx:RepositoriesList.tsx
import { ActionType } from '../action-types';
import { Action } from '../actions';

interface RepositoriesState {
  loading: boolean;
  error: string | null;
  data: string[];
}

const initialState = {
  loading: false,
  error: null,
  data: [],
};

const reducer = (
  state: RepositoriesState = initialState,
  action: Action
): RepositoriesState => {
  switch (action.type) {
    case ActionType.SEARCH_REPOSITORIES:
      return { loading: true, error: null, data: [] };
    case ActionType.SEARCH_REPOSITORIES_SUCCESS:
      return { loading: false, error: null, data: action.payload };
    case ActionType.SEARCH_REPOSITORIES_ERROR:
      return { loading: false, error: action.payload, data: [] };
    default:
      return state;
  }
};

export default reducer;
```
また、ActionのTypeについては、Reducer以外にもActionの型定義やAction Creatorなどの複数のファイルで使用するため、`src/action-types/index.ts`で管理します。

```ts:index.ts
export enum ActionType {
  SEARCH_REPOSITORIES = 'search_repositories',
  SEARCH_REPOSITORIES_SUCCESS = 'search_repositories_success',
  SEARCH_REPOSITORIES_ERROR = 'search_repositories_error',
}
```

`repositoriesReducer.ts`と同じ階層に`index.ts`を作成し、作成したReducer(repositoriesReducer)をcombineReducersでラッピングします。
今後新しくReducerを作成したとしても、CombineReducerに登録するだけで1つのまとまったReducerとして扱うことができます。

```tsx:index.ts
import { combineReducers } from 'redux';
import repositoriesReducer from './repositoriesReducer';

const reducers = combineReducers({
  repositories: repositoriesReducer,
});

export default reducers;

export type RootState = ReturnType<typeof reducers>;
```

####Reducerの型定義
Recucerは引数にStateとAction、戻り値に最新のStateをもつため、それぞれに対して型定義を行う必要があります。

```tsx
const reducer = (
  state: RepositoriesState = initialState,
  action: Action
): RepositoriesState => {
```

まず、Stateの型は以下のように定義します。
`loading`はロード中の可否(true/false)を返すため`boolean`、`data`は空の場合も含めたデータのリストを返すため`string[]`、`error`はエラーメッセージを返す場合と返さない場合があるため`string | null`のUnion型で定義します。

```tsx
interface RepositoriesState {
  loading: boolean;
  error: string | null;
  data: string[];
}
```

試しに`case ActionType.SEARCH_REPOSITORIES:`の戻り値のStateの一部を`loading: null`としてみると、「nullはboolean型じゃありませんよ！」とちゃんと怒ってくれます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/c84ad73e-941a-5b90-b337-d5d394271660.png" width="700">

次に、Actionの型については、3つのAction(SearchRepositoriesAction, SearchRepositoriesSuccessAction, SearchRepositoriesErrorAction)のUnion型として定義します。

SearchRepositoriesSuccessActionとSearchRepositoriesErrorActionについては、検索結果やエラーメッセージを返すため`payload`をもちます。
検索結果はリストのため`string[]`、エラーメッセージは`string`として定義します。

```ts:index.ts
import { ActionType } from '../action-types';

interface SearchRepositoriesAction {
  type: ActionType.SEARCH_REPOSITORIES;
}

interface SearchRepositoriesSuccessAction {
  type: ActionType.SEARCH_REPOSITORIES_SUCCESS;
  payload: string[];
}

interface SearchRepositoriesErrorAction {
  type: ActionType.SEARCH_REPOSITORIES_ERROR;
  payload: string;
}

export type Action =
  | SearchRepositoriesAction
  | SearchRepositoriesSuccessAction
  | SearchRepositoriesErrorAction;

```

###Action Creatorの作成
非同期でDispatch(ActionをReducerに渡すもの)を返すようなAction Creator(`searchRepositories`)を作成します。
一見中身が複雑ですが、処理の流れは以下のようにシンプルです。

1. SearchRepositoriesActionをDispatch(Reducerでロード中のStateに更新)
2. 検索ワード`term`をパラメータとして渡しnpm APIからデータを取得
3. 【データ取得に成功時】SearchRepositoriesActionSuccessをDispatch(Reducerで検索結果のStateに更新)
4. 【データ取得に失敗時】SearchRepositoriesActionErrorをDispatch(ReducerでエラーメッセージのStateに更新)

```ts:index.ts
import axios from 'axios';
import { Dispatch } from 'redux';
import { ActionType } from '../action-types';
import { Action } from '../actions';

export const searchRepositories = (term: string) => {
  return async (dispatch: Dispatch<Action>) => {
    //SearchRepositoriesActionのDispatch(ロード中のStateに更新)
    dispatch({
      type: ActionType.SEARCH_REPOSITORIES,
    });

    try {
      //非同期でnpmのAPIからデータ取得
      const { data } = await axios.get(
        'https://registry.npmjs.org/-/v1/search',
        {
          params: {
            text: term,
          },
        }
      );
  
      //取得データかパッケージ名のみを配列で返す
      const names = data.objects.map((result: any) => {
        return result.package.name;
      });

      //SearchRepositoriesActionSuccessのDispatch(検索結果のStateに更新)
      dispatch({
        type: ActionType.SEARCH_REPOSITORIES_SUCCESS,
        payload: names,
      });
    } catch (err) { //データ取得に失敗した場合
      //SearchRepositoriesActionErrorのDispatch(エラーメッセージのStateに更新)
      dispatch({
        type: ActionType.SEARCH_REPOSITORIES_ERROR,
        payload: err.message,
      });
    }
  };
};
```

####Action Creatorの型定義
dispatchの型定義を`dispatch: Dispatch<Action>`のように行います。
ジェネリクスとして`src/actions/index.ts`で定義したActionの型を渡しています。

```ts
export type Action =
  | SearchRepositoriesAction
  | SearchRepositoriesSuccessAction
  | SearchRepositoriesErrorAction;
```

こうすることで、型定義していないActionを誤って記述してしまったときにWarningを出してくれるようになります。

###Redux側のエンドポイント(`index.ts`)の作成
React/Redux連携の部分で説明した、Redux側のエンドポイントとなるファイルを以下のように作成します。

```ts:index.ts
export * from './store';
export * as actionCreators from './action-creators';
export * from './reducers';
```

これでReact側からこのファイルにアクセスするだけで、StoreとAction CreatorとReducerすべてにアクセスすることができるようになります。
ちなみにStoreの中身は以下のようになっています。

```ts:store.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducers';

export const store = createStore(reducers, {}, applyMiddleware(thunk));
```

##React(表示側)の実装
Reduxの実装ができたので、Reactのコンポーネント側でReduxとの接続を行っていきます。

###RepositoriesList.tsxの作成
検索文字の入力と送信を行って、検索結果を表示するような画面を作成します。
`useActions()`と`useTypedSelector()`というカスタムフックがありますが、これらを使用することでそれぞれStoreのStateの更新、取得を行っています。

Stateの更新と検索結果表示の流れは以下のようになります。

1. `Input`で入力した文字(term)をボタンを押したときにformで送信
2. `onSubmit`内で`useActions()`で定義した`searchRepositories(term)`を実行(Stateの更新)
3. Stateが更新されることで`useTypedSelector()`の`data`も更新
4. 検索結果を表示

```tsx:RepositoriesList.tsx
import { useState } from 'react';
import { useActions } from '../hooks/useActions';
import { useTypedSelector } from '../hooks/useTypedSelector';

const RepositoriesList: React.FC = () => {
  const [term, setTerm] = useState('');
  const { searchRepositories } = useActions();
  const { data, error, loading } = useTypedSelector(
    (state) => state.repositories
  );
  
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    searchRepositories(term);
  };

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input value={term} onChange={(e) => setTerm(e.target.value)} />
        <button>Search</button>
        {error && <h3>{error}</h3>}
        {loading && !loading && data}
        {!error && !loading && data.map((name) => <div key={name}>{name}</div>)}
      </form>
    </div>
  );
};

export default RepositoriesList;
```

###useActions()の作成
`useActions()`は`useDispatch()`と`bindActionCreators`によって、作成したActionをReactコンポーネント内でDispatchできるようにしたカスタムフックです。

`useDispatch()`を使用するとmapDispatchToPropsやconnect関数を書かずにStoreとの接続を行い、ActionをDispatchできるようになります。
`useDispatch()`で定義した`dispatch`を`bindActionCreators`でAction Creatorと接続してあげることで、作成したActionをReactコンポーネント内でDispatchできるようになります。

```ts:useActions.ts
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../state';

export const useActions = () => {
  const dispatch = useDispatch();

  return bindActionCreators(actionCreators, dispatch);
};
```

https://tech.stmn.co.jp/entry/2020/10/29/161055

###useTypedSelector()の作成
`useTypedSelector()`は`useSelector`に型定義を行ったカスタムフックです。

`useSelector()`を使用するとmapStateToPropsやconnect関数を書かずにStoreとの接続を行い、Reactコンポーネント内でStoreのStateを使えるようになります。

```ts:useTypedSelector.ts
import { useSelector, TypedUseSelectorHook } from 'react-redux';
import { RootState } from '../state';

export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
```

`useTypedSelector`の型定義には`TypedUseSelectorHook<T>`を使用し、ジェネリクスとして`RootState`を渡しています。
この`RootState`は`src/reducers/index.ts`内で定義した`ReturnType<typeof reducers>`です。

```ts
export type RootState = ReturnType<typeof reducers>;
```
`RepositoriesList.tsx`内の`useTypedSelector`にカーソルをあてると、以下のように型定義されていることを確認することができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/29681ce4-f281-3c7d-ed3d-a10126005098.png" width="700">

カスタムフックの作成については、以下の記事をあわせて参考にしていただければと思います。

https://qiita.com/suzuki0430/items/49c88a6facad0b3aefd1

##App.tsxの作成
あとは`App.tsx`で`Provider`をラッピングしてあげるだけです。

```tsx:App.tsx
import { Provider } from 'react-redux';
import { store } from '../state';
import RepositoriesList from './RepositoriesList';

const App = () => {
  return (
    <Provider store={store}>
      <div>
        <h1>Search For a Package</h1>
        <RepositoriesList />
      </div>
    </Provider>
  );
};

export default App;
```
最終的な`src`の構成は以下のようになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/547223df-e682-c0ec-cb68-f9880cfe86f1.png" width="300">


#参考資料

https://www.udemy.com/share/103ZeuCEEZd11VRno=/

https://qiita.com/official-events/b9ad63394fa2635dfca9