RTK Queryでデータを取得し、Custom Hooksで操作をカプセル化する
この記事はUMITRON Advent Calendar 2024 6日目の記事です。
まえがき
弊社のプロダクト(以下、弊プロダクト)ではRedux ToolkitのcreateSlice
とcreateAsyncThunk
を使ってAPIから取得したデータの管理を行なっています。しかし、Local Stateの初期化に使われているのみの箇所が多く見られ、無理にRedux Toolkitを使う意味があるのか疑問です。また、Local Stateは素のuseState
で管理されているものが多く、認知負荷の高いコードを生む原因となっています。
今回はRTK QueryでAPIからデータを取得し、Custom Hooksでデータへの操作をカプセル化してみました。
何が問題になっているか
Slice
の立ち位置
揺れるRedux ToolkitのcreateSlice
はstateとreducerからaction creatorとaction typeを生成してSlice
として返す関数です。本来はactionとreducerがまとめられる便利ツールなのですが、弊プロダクトではcreateAsyncThunk
と一緒に使うことが前提となっていて、以下の例のように非同期で取得したデータを置く場所(置く場所はstateか)のようになっています。
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const itemsAdapter = createEntityAdapter<Item>();
const items = createSlice({
name: 'items',
initialState: itemsAdapter.getInitialState(),
reducers: {}, // reducerがない!!!
extraReducers: (builder) => {
builder
.addCase(fetchItems.fulfilled, itemsAdapter.setAll)
.addCase(createItems.fulfilled, itemsAdapter.addOne)
.addCase(deleteItems.fulfilled, itemsAdapter.removeOne)
.addCase(updateItems.fulfilled, itemsAdapter.upsertOne);
},
});
export const { selectAll } = itemsAdapter.getSelectors(
(state: State) => state.items
);
export default items.reducer;
せっかくSlice
を作っても以下のようにLocal Stateの初期化に使われているだけというケースが多く、記述量が増えているだけのように思えます。Thunk Actionは複数のSlice
で受けることも可能なので、取得したデータをそのまま表示する画面と取得したデータを書き換える画面があり、後者の状態を前者に反映したくないといった場合、それぞれに対してSlice
を与えて対応することができます。Redux的にはおそらくそれが正攻法なのかもしれない(そうでもないかもしれない)ですが、画面から離れたら破棄されるデータをGlobal Stateに持たせたくありません。
const ItemsEditView = () => {
const dispatch = useDispatch();
const [items, setItems] = useState<Item[]>([]);
const initialItems = useSelector(selectAllItems);
// `fetch`でよくない?
useEffect(() => {
dispatch(fetchItems());
}, []);
useEffect(() => {
setItems(initialItems);
}, [initialItems]);
return ...;
};
Local Stateに対するプリミティブな操作
直接useState
を使ってしまうと配列に対するプリミティブな操作がそのままコンポーネントに記述されてしまいがちです。見るからにコンポーネントの責務ではありませんし、他のコンポーネントでも同じような操作をするでしょうから、どうにかして分けたいです。
const ItemsEditView = () => {
const dispatch = useDispatch();
const [items, setItems] = useState<Item[]>([]);
const initialItems = useSelector(selectAllItems);
...
// 実際にはこんなものでは済まない
const updateItem = (id: number, name: string) => {
setItems((items) =>
items.map((item) => (item.id === id ? { ...item, name } : item))
);
};
return ...;
}
やったこと
RTK Queryを使ってデータを取得し、キャッシュする
Redux ToolkitにはRTK Queryというデータの取得とキャッシュに特化したツールがあります。APIから取得したデータをキャッシュしてくれるのに加えて、内部的にcreateAsyncThunk
を使っているため任意のSlice
のextraReducers
でactionをキャッチすることも可能です。endpoints
に記述したquery
やmutation
はhooksとして使うことができます。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (build) => ({
// `api.endpoints.getItems.matchFulfilled`でキャッチできる
getItems: build.query<Item[], void>({
query: () => 'items',
}),
updateItems: build.mutation<void, Item[]>({
query: (items) => ({
url: `items`,
method: 'PUT',
body: items,
}),
}),
}),
});
export const { useGetItemsQuery, useUpdateItemsMutation } = api;
default export api;
Custom Hookで操作をカプセル化する
データの取得と操作をCustom Hooksでカプセル化しました。RTK Queryのhooksで初期化し、CRUD操作はcreateEntityAdapter
を使うことで実装を省略しています。
import { createEntityAdapter } from '@reduxjs/toolkit';
import { useCallback, useEffect, useState } from 'react';
export const useItems = () => {
const { data = [] } = useGetItemsQuery();
const [updateItemsMutation] = useUpdateItemsMutation();
// `createEntityAdapter`はどうしても使いたかった
const itemsAdapter = createEntityAdapter<Item>();
const [itemsState, setItemsState] = useState(itemsAdapter.getInitialState());
useEffect(() => {
setItemsState(itemsAdapter.setAll(itemsState, data));
}, [data]);
const items = itemsAdapter.getSelectors().selectAll(itemsState);
const updateItem = (item: Item) => {
itemsAdapter.updateOne(itemsState, { id: item.id, changes: item });
};
const saveItems = useCallback(() => {
updateItemsMutation(items);
}, [items]);
return { items, updateItem, saveItems } as const;
};
まとめ
今回はRedux Toolkitをすでに使っているという都合上RTK Queryを使いましたが、データをキャッシュしたい!hooksとして使いたい!という話であればSWRを使うのも手なのかなと思いました。
また、Custom Hooksは雑に使えて便利ですが、必要に応じてuseMemo
やuseCallback
を使わないとパフォーマンスの問題も生じてきそうなのでzustandを使うのもよさそうです。
私は業務でReactに向き合うようになったのはここ2年くらいで、ここ最近はいい加減Reduxを完全に理解しなくてはと考えていましたが、無理にReduxを頑張らなくてもいろいろな選択肢があるのだなと思いました。
参考
スコープとライフタイムで考えるReact State再考
ReduxにおけるGlobal stateとLocal stateの共存
駄目になった Redux Thunk を hooks でリファクタリングする
「3種類」で管理するReactのState戦略
「ReactのカスタムHooksをカジュアルに使う」という記事を読んでカジュアルに使ってみた
ファーストクラスコレクション
Redux Architecture Guidelines
ウミトロンは、「持続可能な水産養殖を地球に実装する」というミッション実現に向けて、日々プロダクト開発・展開にチーム一丸となって邁進しています。
ウミトロンのニュースや活動状況を各種SNSで配信していますので、ぜひチェックいただき、来年も応援よろしくお願いします!
Facebook https://www.facebook.com/umitronaqtech/
X https://x.com/umitron
Instagram https://www.instagram.com/umitron.aqtech/
Linkedin https://www.linkedin.com/company/umitron
Discussion