目次
使用感
React標準のContextから乗り換えたので、主にその比較
Pros
- きちんと設計すれば管理が楽になる
- コンポーネント側での状態管理が快適になり、コードがかなりスッキリした
- 操作が「動作中である」という状態も簡単に管理できる(
redux-thunk利用の場合) - ストア側で、状態への変更のされ方を制限/管理でき
Cons
- 学習コストが高め(Store, Action, Dispatch...) ; 情報の整理に時間がかかる
- 上記基本的な概念に加えて加えて、Slice等のオプショナルなものがあったり
- 解説ブログ等でactionというが概念が大事そうに書かれているのに、
@reduxjs/toolkitを使用する関係でActionを記述する必要がなかったり
- 設計が複雑 ; 依存関係の切り分けが難しい
- データの書き換えを行う
reducer、reducerのための前処理を行うthunkが存在し、細かいケースで「これはどっちの担当なんだ?」となった
- データの書き換えを行う
Redux用語の解説
-
store- アプリにつき一つだけ持つ、状態や下記のreducer等を全て管理するボス
-
slice- 状態管理するのが唯一の
storeだけだとコード管理が大変だ ということで生み出されたstoreの子分- 機能は
storeとあまり変わらない
- 機能は
- 管理対象の分野ごとに複数持ち、storeに合流させる
- 状態管理するのが唯一の
-
reducer- 実際に状態の変更を行う関数
-
dispatch- Reactコンポーネントから「状態変更したいこと」と
storeに伝える関数
- Reactコンポーネントから「状態変更したいこと」と
-
action- 「どのように状態変更したいのか」を
storeが識別するためのオブジェクト -
reducerと一対一関係にあるtypeと、reducerに渡したい値payloadを属性を持つ
- 「どのように状態変更したいのか」を
-
thunk-
redux-thunkライブラリに入っている : ピュアなreact-reduxには無い -
reducerの兄弟- ピュア
reduxのreducerは、非同期関数を扱えないが、こいつは使える -
reducerとほぼ同様に扱える
- ピュア
-
まとめると
-
redux側では-
store,sliceで状態を保管し、 -
reducerで状態を変更する
-
-
reactコンポーネント側では-
dispatchで状態を変更する
-
-
actionがこれらの間を取り持つ
コード設計(@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
コードサンプル(一部抜粋)
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());
}
}
);
終わりに
- 分かりづらい点、間違っている点など、お気づきの点がございましたらご指摘いただけますと幸いです