この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おはようございます、もきゅりんです。
大分前に、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