Redux Toolkit また(自分が)忘れないための基本まとめ
はじめに
おはようございます、もきゅりんです。
大分前に、Redux Toolkit (以後 RTK )を見様見真似で触っていたのですが、改めて最近使おうとしたら、ほとんど記憶から失われていました。
自分の脳みそは、普段あまり使わない記憶をあっという間に風化させてしまいます。(普段フロントエンドを扱うことはほぼないため)
そんなこともあり、きっとまたしばらく手つかずにすると記憶から風化する、と予想されたので、未来の自分のために、今の自分がまとめておこうと考えました。
そして、もしかしたら、自分のように、大分前に RTK 触ったけど、何だか覚えていない、、って方や RTK ってどんなものなの?って興味ある人には、もしかしたらお役に立つかもしれない、ということでブログにしておきました。
RTK の導入の仕方は、Getting Started や、TypeScript Quick Start を見ると、割りとスムーズです。
では、何が記憶から薄れるのか?
- (個人的に感じる)似たような用語
- それらの関係
です。
なので、関係図を眺め、サンプルのコードと照らし合わせて思い出すように、未来の自分へ促します。
なお、この記憶喚起するブログには RTK Query は入っていません。
関係図を眺める
とりあえず、これを眺めてどんなものか思い出しましょう。
お分かり頂けただろうか?
わざわざ下図と色を合わせて作図しました。
出典: Redux Essentials, Part 5: Async Logic and Data Fetching | Redux
この図は以下のようなフローを示しています。
- UI からのイベント発火
dispatch
から 指定のaction
を実行- store は
action
と現state
からreducer
を実行して新しくstate
を生成
各 state
の値を selector
を用いて UI に反映させます。
コードと見比べる
図と併せて、図に描かれた概念を上から順に Redux Essentials のコードを使って確認しましょう。
なんでこのチュートリアル、TypeScript で書いてないんだろう、と思って、Redux Essentials と Redux Fundamentals の両方、エラーが出ない程度の最低限の TypeScript にしました。
Redux + TypeScript template で開始すると、State とか useSelector、useDispatch Hooks とかも型定義してくれていて導入がスムーズです。
npx create-react-app my-app --template redux-typescript
Store
Reducer が Store に集約されています。
import postsReducer from '../features/posts/postsSlice' import usersReducer from '../features/users/usersSlice' import notificationsReducer from '../features/notifications/notificationsSlice' export const store = configureStore({ reducer: { posts: postsReducer, users: usersReducer, notifications: notificationsReducer, }, });
各 Reducer は 各 State と それに影響を与える Action(Dispatch) を管理する概念です。
アプリに必要となる State それぞれで Reducer を管理しますが、 State をどうするかはしっかり考えるべきポイントです。
Designing the State Values でも、下記のように述べられています。
ReactとReduxのコア原則の1つは、UIが State に基づいている必要があるということです。したがって、アプリケーションを設計するための1つのアプローチは、最初にアプリケーションがどのように機能するために必要なすべての State を考えることです。また、State 内の値をできるだけ少なくして UI を設計し、追跡および更新する必要のあるデータが少なくなるようにすることもお勧めします。 (邦訳)
そして、index.tsx で store が App 全体を Wrap しています。
import { store } from './app/store'; import { Provider } from 'react-redux'; import * as serviceWorker from './serviceWorker'; import { fetchUsers } from './features/users/usersSlice'; import { worker } from './api/server'; // Start our mock API server worker.start({ onUnhandledRequest: 'bypass' }); store.dispatch(fetchUsers()); ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') );
App
このアプリでは、 App.tsx は Redux に関係ないのでスキップします。
Components
Components という名前が components ディレクトリかと紛らわしいけど、 features ディレクトリを見ます。
Redux Style Guide の Structure Files as Feature Folders with Single-File Logic にも feature folder で Reducer ロジックは1ファイルにまとめることを強く推奨としています。
PostsList.tsx を見ると postsSlice から state や dispatch を import しています。
このように、各 React コンポーネントは、 必要となる state と連携し、 Reducer はコンポーネントからの dispatch を通じて state を再生成します。
import { useAppSelector, useAppDispatch } from '../../app/hooks'; import { fetchPosts, selectPostIds, selectPostById } from './postsSlice'; import { Link } from 'react-router-dom'; import { useEffect } from 'react'; import { EntityId } from '@reduxjs/toolkit'; // 省略 export const PostsList = () => { const dispatch = useAppDispatch(); const orderedPostIds = useAppSelector(selectPostIds); const postStatus = useAppSelector((state) => state.posts.status); const error = useAppSelector((state) => state.posts.error); useEffect(() => { if (postStatus === 'idle') { dispatch(fetchPosts()); } }, [postStatus, dispatch]); let content;
Reducer
Redux の reducer のロジックに関係するファイルは、xxxSlice.ts (小文字で始まる) です。
RTK の根幹になります。
ここでは、各 State に対して createEntityAdapter を生成して以下のように管理します。
なぜそうするべきなのか気になったら、 Redux Essentials, Part 6: Performance and Normalizing Data を再度読みましょう。
{ // The unique IDs of each item. Must be strings or numbers ids: [] // A lookup table mapping entity IDs to the corresponding entity objects entities: { } }
基本的な CRUD 操作に関する関数を揃えているだけでなく、 汎用的な State を getSelectors
関数を使って取得できます。
const postsAdapter = createEntityAdapter<Post>({ sortComparer: (a, b) => b.date.localeCompare(a.date), }); // 省略 // Export the customized selectors for this adapter using `getSelectors` export const { selectAll: selectAllPosts, selectById: selectPostById, selectIds: selectPostIds, // Pass in a selector that returns the posts slice of state } = postsAdapter.getSelectors((state: RootState) => state.posts);
さらに、引数に上記の selector 関数 を使った createSelector
で、いい感じのデータをまとめられます。
export const selectPostsByUser = createSelector( [selectAllPosts, (state: RootState, userId: EntityId) => userId], (posts, userId) => posts.filter((post) => post.user === userId) );
これらを React コンポーネントで useSelector などを使って読み込んで UI を更新し、その更新された値に対して、またイベントを発火して再び State を更新していきます。
UI 上でイベント発火された際に State を Reduce するための dispatch , action を createSlice
で生成します。
const postsSlice = createSlice({ name: 'posts', initialState, reducers: { reactionAdded( state, action: PayloadAction<{ postId: string; reaction: keyof Reaction }> ) { const { postId, reaction } = action.payload; const existingPost = state.entities[postId]; if (existingPost) { existingPost.reactions[reaction]++; } }, postUpdated( state, action: PayloadAction<Pick<Post, 'id' | 'title' | 'content'>> ) { const { id, title, content } = action.payload; const existingPost = state.entities[id]; if (existingPost) { existingPost.title = title; existingPost.content = content; } }, },
非同期的な関数を reducer で扱いたい場合は、 createAsyncThunk
を定義して、createSlice に extraReducers
オプションで対応します。
export const fetchPosts = createAsyncThunk<Post[]>( 'posts/fetchPosts', async () => { const response = await client.get('/fakeApi/posts'); return response.data; } ); // 省略 extraReducers(builder) { builder .addCase(fetchPosts.pending, (state, action) => { state.status = 'loading'; }) .addCase(fetchPosts.fulfilled, (state, action) => { state.status = 'succeeded'; // Add any fetched posts to the array postsAdapter.upsertMany(state, action.payload); }) .addCase(fetchPosts.rejected, (state, action) => { state.status = 'failed'; if (action.error.message) { state.error = action.error.message; } });
これは見れば比較的に特徴的なので、すぐ思い出せる箇所と思います。
以上で冒頭の図の説明は終わりです、思い出したでしょうか。
使い方の詳細は Redux Toolkit の API Reference を見ましょう。
これでしばらく時を経ても、このブログを見れば、スムーズにやりたいことに着手できるのではないでしょうか。
そして、次回これを見るときにめちゃめちゃ RTK の仕様が変わっていないことを祈っています。
以上です。
未来の自分のためのまとめではありますが、どなたかのお役に立てば幸いです。
参考
- Typescript ブラケット記法(Object[key])でno index signatureエラーをtype safeに解決したい。 - aknow2
- Typescript Property includes does not exist on type string - Stack Overflow
- TypeScript: string | undefinedな配列からundefinedを取り除く処理の型付けをしっかりする方法 - Qiita
- TypeScriptの組み込み型関数 Pickの使いどころ - Qiita
- プロパティの初期値をnullにしたりしなかったりの使い分け - Qiita