目次
使用感
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());
}
}
);
終わりに
- 分かりづらい点、間違っている点など、お気づきの点がございましたらご指摘いただけますと幸いです