0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-10-23

目次

  1. 使用感
  2. 用語解説
  3. コード設計例
  4. コードサンプル
  5. 終わりに

使用感

React標準のContextから乗り換えたので、主にその比較

Pros

  • きちんと設計すれば管理が楽になる
    • コンポーネント側での状態管理が快適になり、コードがかなりスッキリした
  • 操作が「動作中である」という状態も簡単に管理できる(redux-thunk利用の場合)

Cons

  • 学習コストが高め(Store, Action, Dispatch...) ; 情報の整理に時間がかかる
    • 上記基本的な概念に加えて加えて、Slice等のオプショナルなものがあったり
    • 解説ブログ等でactionというが概念が大事そうに書かれているのに、@reduxjs/toolkitを使用する関係でActionを記述する必要がなかったり
  • 設計が複雑 ; 依存関係の切り分けが難しい
    • データの書き換えを行うreducerreducerのための前処理を行うthunk が存在し、細かいケースで「これはどっちの担当なんだ?」となった

Redux用語の解説

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

まとめると

  • redux側では
    • store, sliceで状態を保管し、
    • reducerで状態を変更する
  • reactコンポーネント側では
    • dispatchで状態を変更する
  • actionがこれらの間を取り持つ

コード設計(@reduxjs/toolkitライブラリ使用)

  • Twitterライクなアプリでタグ、ポストを管理する想定
    • タグを使用したフィルタリング機能を持つ
  • あくまで、「私はこうするといいと思います」くらいの内容です

大事なところ

  • toolkitを使う場合、actionを記述する必要はない
  • sliceに記述するreducerは、stateへのピュアな代入のみ ; 少しでも条件分岐するなら、基本的にロジック部分はthunk書いて、状態変更をthunk内でdispatchする
    • 結果として、sliceextraReducersには基本的に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部分

  • toolkitcreateSlice関数を使うと、actionの記述は不要です
  • reducer部分が多すぎる場合は、別ファイルに記述すると良いでしょう
  • reducersに所謂reducerを、extraReducersにはthunkthunk呼び出し後に実行される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を取ってきて、設定するだけ
  • RootStateAppDispatch型は、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関数を利用します
    • actionthunkをセットで作ってくれます
    • 第一引数はaction.typeにあたる、reducerthunkについて一対一対応する任意の文字列を指定する
    • 型指定の一つ目は、return値の型; slicerextraReducersにこの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());
		}
	}
);

終わりに

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?