[React]状態管理ライブラリ Reduxを使ってみて / toolkitを踏まえたコード設計

# 目次
1. [使用感](#使用感)
2. [用語解説](#redux用語の解説)
3. [コード設計例](#コード設計reduxjstoolkitライブラリ使用)
4. [コードサンプル](#コードサンプル一部抜粋)
5. [終わりに](#終わりに) 

<a id="anc1"></a>

# 使用感
React標準のContextから乗り換えたので、主にその比較
#### Pros
- きちんと設計すれば管理が楽になる
    - コンポーネント側での状態管理が快適になり、コードがかなりスッキリした
- 操作が「動作中である」という状態も簡単に管理できる（`redux-thunk`利用の場合）
- ストア側で、状態への変更のされ方を制限/管理でき
#### Cons
- 学習コストが高め(Store, Action, Dispatch...) ; 情報の整理に時間がかかる
    - 上記基本的な概念に加えて加えて、Slice等のオプショナルなものがあったり
    - 解説ブログ等でactionというが概念が大事そうに書かれているのに、`@reduxjs/toolkit`を使用する関係でActionを記述する必要がなかったり
- 設計が複雑 ; 依存関係の切り分けが難しい
    - データの書き換えを行う`reducer`、`reducer`のための前処理を行う`thunk` が存在し、細かいケースで「これはどっちの担当なんだ？」となった

<a id="anc2"></a>

# Redux用語の解説
- `store`
    - アプリにつき一つだけ持つ、状態や下記のreducer等を全て管理するボス
- `slice`
    - 状態管理するのが唯一の`store`だけだとコード管理が大変だ ということで生み出されたstoreの子分
        - 機能は`store`とあまり変わらない
    - 管理対象の分野ごとに複数持ち、storeに合流させる
- `reducer`
    - 実際に状態の変更を行う関数
- `dispatch`
    - Reactコンポーネントから「状態変更したいこと」と`store`に伝える関数
- `action`
    - 「どのように状態変更したいのか」を`store`が識別するためのオブジェクト
    - `reducer`と一対一関係にある`type`と、reducerに渡したい値`payload`を属性を持つ
- `thunk`
    - **`redux-thunk`ライブラリに入っている** : ピュアな`react-redux`には無い
    - `reducer`の兄弟
        - ピュア`redux`の`reducer`は、非同期関数を扱えないが、こいつは使える
        - `reducer`とほぼ同様に扱える
### まとめると
- `redux`側では
    - `store`, `slice`で状態を保管し、
    - `reducer`で状態を変更する
- `react`コンポーネント側では
    - `dispatch`で状態を変更する
- `action`がこれらの間を取り持つ

<a id="anc3"></a>

# コード設計（`@reduxjs/toolkit`ライブラリ使用）
- Twitterライクなアプリでタグ、ポストを管理する想定
    - タグを使用したフィルタリング機能を持つ
- あくまで、「私はこうするといいと思います」くらいの内容です
## 大事なところ
- `toolkit`を使う場合、`action`を記述する必要はない
- **`slice`に記述する`reducer`は、`state`へのピュアな代入のみ** ; **少しでも条件分岐するなら、基本的にロジック部分は`thunk`に**書いて、状態変更を`thunk`内で`dispatch`する
    - 結果として、`slice`の`extraReducers`には基本的に`builder.addCase(exampleThunk.fulfilled, ()=>{});`のような空の関数が並ぶ
    - 勿論、`pending`等の状態を特別に管理したい場合は、`thunk`側のコールバック`reducer`関数に記述しましょう
## 大まかなファイル構成
```
.
～～～
├── slice                    // reducerはここに記述します
│   ├── tags.tsx
│   └── tweets.tsx
├── store
│   └── index.ts
└── thunk
    ├── deleteTweet.tsx
    ├── loadTweet.tsx
    ├── postTweet.tsx
    └── setTagFilter.tsx
```

<a id="anc4"></a>

## コードサンプル（一部抜粋）
### `slice`部分
- `toolkit`の`createSlice`関数を使うと、`action`の記述は不要です
- `reducer`部分が多すぎる場合は、別ファイルに記述すると良いでしょう
- `reducers`に所謂`reducer`を、`extraReducers`には`thunk`と`thunk`呼び出し後に実行される`reducer`関数を記述します
```typescript slice/tweets.ts
import type { TweetWithTags } from '../../../types/Tweet';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { loadOlderTweetsAction, loadNewerTweetsAction, reloadTweetsAction } from '../thunk/loadTweet';
import { postTweetAction } from '../thunk/postTweet';
import { deleteTweetAction } from '../thunk/deleteTweet';
import { applyTagFilterAction } from '../thunk/setTagFilter';

export interface TweetSliceState {
	tweets: TweetWithTags[];
	tagFilter: string | null;
}

const initialState: TweetSliceState = {
	tweets: [],
	tagFilter: null,
}

const tweetsSlice = createSlice({
	name: 'tweets',
	initialState: initialState,
	reducers: {
		removeTweet: (state: TweetSliceState, action: PayloadAction<string>) => {
			state.tweets = state.tweets.filter(tweet => tweet.id !== action.payload);
		},
		setTweets: (state: TweetSliceState, action: PayloadAction<TweetWithTags[]>) => {
			state.tweets = action.payload;
		},
		pushBackTweets: (state: TweetSliceState, action: PayloadAction<TweetWithTags[]>) => {
			state.tweets = [...state.tweets, ...action.payload];
		},
		pushForwardTweets: (state: TweetSliceState, action: PayloadAction<TweetWithTags[]>) => {
			state.tweets = [...action.payload, ...state.tweets];
		},
		setTagFilter: (state: TweetSliceState, action: PayloadAction<string | null>) => {
			state.tagFilter = action.payload;
		},
	},
	extraReducers: builder => {
		builder.addCase(loadOlderTweetsAction.pending, (state, action) => {});
		builder.addCase(loadOlderTweetsAction.fulfilled, (state, action) => {});
		builder.addCase(loadOlderTweetsAction.rejected, (state, action) => {});
		builder.addCase(loadNewerTweetsAction.pending, (state, action) => {});
		builder.addCase(loadNewerTweetsAction.fulfilled, (state, action) => {});
        // ～～～～ （略）～～～～
		builder.addCase(applyTagFilterAction.rejected, (state, action) => {});
	}
})

export const { removeTweet, setTweets, pushBackTweets, pushForwardTweets, setTagFilter } = tweetsSlice.actions;
export const TweetReducer = tweetsSlice.reducer;
```
### `store`部分
- `slice`から`reducer`を取ってきて、設定するだけ
- `RootState`や`AppDispatch`型は、Reactコンポーネント側で利用します
```typescript store/index.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { TweetReducer  } from '../slice/tweets';
import type { TweetSliceState } from '../slice/tweets';
import { TagsReducer } from '../slice/tags';
import type { TagsSliceState } from '../slice/tags';

export interface RootState {
	tweet: TweetSliceState;
	tag: TagsSliceState;
}

const rootReducer = combineReducers({
	tweet: TweetReducer,
	tag: TagsReducer,
});

const store = configureStore({
	reducer: rootReducer,
});

export type AppDispatch = typeof store.dispatch;

export default store;
```
### thunk部分
- `createAsyncThunk`関数を利用します
    - `action`と`thunk`をセットで作ってくれます
    - 第一引数は`action.type`にあたる、`reducer`や`thunk`について一対一対応する任意の文字列を指定する
    - 型指定の一つ目は、return値の型; `slicer`で`extraReducers`にこの`thunk`を設定するときの`reducer`や、`dispatch`の呼び出し元から取り出せます
    - 型指定の二つ目は、引数の値; `thunk`内で参照します
```typescript thunk/postTweet.ts
import { createAsyncThunk } from '@reduxjs/toolkit';
import type { TweetWithTags } from '../../../types/Tweet';
import type { TweetSliceState } from '../slice/tweets';
import { postTweet } from '../repository/postTweet';
import { applyTagFilterAction } from './setTagFilter';
import { reloadTweetsAction } from './loadTweet';

export const postTweetAction = createAsyncThunk<void, TweetWithTags>(
	'tweets/post',                               // 一意のアクション名を記述する
	async (tweet, { rejectWithValue, getState, dispatch }) => {
		const result = await postTweet(tweet);   // データベースに保存
		if (result.status !== 200) {             // 失敗した場合
			return rejectWithValue(result);
		}
		const state = getState() as TweetSliceState;
        // Postされたツイートを見れるよう、タグフィルターに注意してリロードする処理
		let tagFilter = state.tagFilter;
		if (tagFilter !== null && !tweet.tag_id_list.includes(tagFilter)) {
			await dispatch(applyTagFilterAction(null));
		} else {
			await dispatch(reloadTweetsAction());
		}
	}
);
```

<a id="anc5"></a>

# 終わりに
- 分かりづらい点、間違っている点など、お気づきの点がございましたらご指摘いただけますと幸いです