tRPC mutationの楽観的更新スキル。キャッシュ操作、ロールバック、Realtime競合対策を支援。
tRPC + TanStack Queryを使用した楽観的更新(Optimistic Updates)の実装を支援するスキル。
以下の状況で自動発動:
ユーザー操作に対応する全mutationで楽観的更新を実装する
楽観的更新により、ユーザーはサーバーレスポンスを待たずに即座にUIフィードバックを得られる。 これは体感速度を200-800ms改善し、アプリケーションの応答性を大幅に向上させる。
import { api } from '@/lib/trpc';
export function useCreateEntity() {
const utils = api.useUtils();
return api.entity.create.useMutation({
// 1. 楽観的更新
onMutate: async (input) => {
// 進行中のクエリをキャンセル(競合防止)
await utils.entity.list.cancel();
// 現在のキャッシュをスナップショット(ロールバック用)
const previous = utils.entity.list.getData();
// キャッシュを楽観的に更新
utils.entity.list.setData(undefined, (old) => {
if (!old) return old;
const tempId = `temp-${Date.now()}`;
return [
...old,
{
id: tempId,
...input,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
];
});
return { previous };
},
// 2. エラー時ロールバック
onError: (_err, _input, context) => {
if (context?.previous) {
utils.entity.list.setData(undefined, context.previous);
}
},
// 3. 完了時に再検証
onSettled: () => {
void utils.entity.list.invalidate();
},
});
}
export function useUpdateEntity() {
const utils = api.useUtils();
return api.entity.update.useMutation({
onMutate: async ({ id, data }) => {
await utils.entity.list.cancel();
await utils.entity.getById.cancel({ id });
const previousList = utils.entity.list.getData();
const previousItem = utils.entity.getById.getData({ id });
// リストキャッシュを更新
utils.entity.list.setData(undefined, (old) => {
if (!old) return old;
return old.map((item) =>
item.id === id ? { ...item, ...data, updated_at: new Date().toISOString() } : item,
);
});
// 個別キャッシュも更新
utils.entity.getById.setData({ id }, (old) => {
if (!old) return old;
return { ...old, ...data, updated_at: new Date().toISOString() };
});
return { previousList, previousItem };
},
onError: (_err, { id }, context) => {
if (context?.previousList) {
utils.entity.list.setData(undefined, context.previousList);
}
if (context?.previousItem) {
utils.entity.getById.setData({ id }, context.previousItem);
}
},
onSettled: (_data, _err, { id }) => {
void utils.entity.list.invalidate();
void utils.entity.getById.invalidate({ id });
},
});
}
export function useDeleteEntity() {
const utils = api.useUtils();
return api.entity.delete.useMutation({
onMutate: async ({ id }) => {
await utils.entity.list.cancel();
const previous = utils.entity.list.getData();
// リストから即座に削除
utils.entity.list.setData(undefined, (old) => {
if (!old) return old;
return old.filter((item) => item.id !== id);
});
return { previous };
},
onError: (_err, _input, context) => {
if (context?.previous) {
utils.entity.list.setData(undefined, context.previous);
}
},
onSettled: () => {
void utils.entity.list.invalidate();
},
});
}
Supabase Realtimeと楽観的更新を併用する場合、競合を防ぐためにフラグを使用する。
// stores/useEntityCacheStore.ts
import { create } from 'zustand';
interface EntityCacheStore {
isMutating: boolean;
setMutating: (value: boolean) => void;
}
export const useEntityCacheStore = create<EntityCacheStore>((set) => ({
isMutating: false,
setMutating: (value) => set({ isMutating: value }),
}));
// hooks/useEntityRealtime.ts
export function useEntityRealtime() {
const utils = api.useUtils();
const isMutating = useEntityCacheStore((s) => s.isMutating);
useEffect(() => {
const channel = supabase
.channel('entities')
.on('postgres_changes', { event: '*', schema: 'public', table: 'entities' }, () => {
// mutation中はRealtimeによるキャッシュ更新をスキップ
if (!isMutating) {
void utils.entity.list.invalidate();
}
})
.subscribe();
return () => {
void supabase.removeChannel(channel);
};
}, [isMutating, utils]);
}
// mutationでフラグを管理
export function useCreateEntity() {
const utils = api.useUtils();
const setMutating = useEntityCacheStore((s) => s.setMutating);
return api.entity.create.useMutation({
onMutate: async (input) => {
setMutating(true); // mutation開始
// ... 楽観的更新
},
onSettled: () => {
setMutating(false); // mutation完了
void utils.entity.list.invalidate();
},
});
}
以下のケースでは楽観的更新を適用しない:
不可逆操作: アカウント削除、支払い処理など
サーバー計算が必要: IDの発行、複雑な集計など
低頻度操作: 月1回程度の設定変更など
新規mutation作成時:
実装時:
onMutateでキャッシュをスナップショットonErrorでロールバックonSettledで再検証(invalidate)src/features/tags/hooks/
├── useTagCrudMutations.ts # CRUD楽観的更新(作成・更新・削除を統合)
├── useTagMergeMutation.ts # マージ操作の楽観的更新
├── useTagsOptimistic.ts # キャッシュ操作ヘルパー
└── useTagOperations.ts # 高レベル操作(UI連携)
/store-creating - Zustandストア作成/trpc-router-creating - tRPCルーター作成