1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【SWR】楽観的更新で一時IDが残る問題と解決策

Last updated at Posted at 2026-01-26

はじめに

CloudWatchのエラーログを確認していたら、見覚えのない400エラーがあって画面上は正常に動作しているのに、なぜかAPIが400を返しているという問題がありました。

調べてみると、原因は楽観的更新で使っていた一時IDでした!!

本記事では、この問題の原因と解決策を共有します。

この記事で解決する問題

楽観的更新では、サーバーからIDが返ってくる前にUIを更新するために、クライアント側で一時的なID(例: temp-xxx)を生成することがあります。

この一時IDが親子関係を持つデータ構造で伝播すると、子データの取得時に存在しないIDへAPIリクエストが飛んでしまいます。

楽観的更新とは

optimistic-update-jp.png

楽観的更新は、APIリクエストの完了を待たずに、UIを先に更新する手法のことです。

通常の更新:

  • ユーザー操作 → APIリクエスト → レスポンス待ち → UI更新

楽観的更新:

  • ユーザー操作 → UI即時更新 → APIリクエスト(バックグラウンド)

最大のメリットはユーザーが操作の結果を即座に確認できて、アプリケーションが高速に感じられることです。

SWRやTanStack Queryなどのデータフェッチングライブラリでは、この楽観的更新を簡単に実装できる仕組みが提供されています。

問題が起きた状況

データ構造:プロジェクト → タスク → サブタスク(親子関係)

  1. 新しいタスクを作成する
  2. 楽観的更新により、サーバー応答前に仮のID(temp-xxx) でUIに追加される
  3. 直後に、そのタスクのサブタスク一覧を取得しようとする
  4. /api/tasks/temp-xxx/subtasks というリクエストが飛ぶ
  5. サーバーには temp-xxx というIDは存在しない → 400 Bad Request

図解で解説

解決方法

Before(問題のあるコード)

hooks/swr/useSubtasksSWR.ts
export function useSubtasksSWR(taskId: string) {
  return useSWR(
    ['subtasks', taskId],
    () => subtasksApi.list(taskId)
  );
}
hooks/swr/useProjectSWR.ts
export function useProjectSWR(projectId: string) {
  return useSWR(
    ['project', projectId],
    () => projectsApi.get(projectId)
  );
}
hooks/mutations/useTasksMutation.ts
export function useTasksMutation() {
  const createTask = async (projectId: string, taskData: TaskInput) => {
    const tempId = `temp-${Date.now()}`;

    // 楽観的にキャッシュを更新
    mutate(
      ['project', projectId],
      (project) => ({
        ...project,
        taskIds: [...project.taskIds, tempId],
      }),
      { revalidate: false }
    );

    const newTask = await tasksApi.create(projectId, taskData);

    // 本物のIDで置き換え
    mutate(['project', projectId]);

    return newTask;
  };

  return { createTask };
}
components/SubtaskList.tsx
function SubtaskList({ taskId }: { taskId: string }) {
  // taskId が "temp-xxx" の場合、存在しないIDへリクエストが飛ぶ
  const { data: subtasks } = useSubtasksSWR(taskId);

  return <ul>{subtasks?.map(s => <li key={s.id}>{s.title}</li>)}</ul>;
}
components/TaskList.tsx
function TaskList({ projectId }: { projectId: string }) {
  const { data: project } = useProjectSWR(projectId);
  const { createTask } = useTasksMutation();

  const handleAddTask = () => {
    createTask(projectId, { title: '新しいタスク' });
  };

  if (!project) return null;

  return (
    <div>
      <button onClick={handleAddTask}>タスク追加</button>

      {project.taskIds.map(taskId => (
        <div key={taskId}>
          <TaskItem taskId={taskId} />
          <SubtaskList taskId={taskId} /> {/* temp-xxx でAPIエラー */}
        </div>
      ))}
    </div>
  );
}

After(修正後のコード)

components/TaskList.tsx
function TaskList({ projectId }: { projectId: string }) {
  const { data: project } = useProjectSWR(projectId);
  const { createTask } = useTasksMutation();

  const handleAddTask = () => {
    createTask(projectId, { title: '新しいタスク' });
  };

  if (!project) return null;

  // 一時IDを除外
  const confirmedTaskIds = project.taskIds.filter(
    (id) => !id.startsWith('temp-')
  );

  return (
    <div>
      <button onClick={handleAddTask}>タスク追加</button>

      {confirmedTaskIds.map(taskId => (
        <div key={taskId}>
          <TaskItem taskId={taskId} />
          <SubtaskList taskId={taskId} /> {/* 確定したIDのみ */}
        </div>
      ))}
    </div>
  );
}

ポイント

  • 一時ID(temp-)はUIの即時反映用
  • APIリクエストを発行する箇所では、一時IDを除外する
  • フィルタリングは「APIを呼ぶ直前」で行う

まとめ

問題

楽観的更新で使用する一時ID(temp-xxx)が親子関係を持つデータで子要素に伝播し、存在しないIDへのAPIリクエストが発生して400エラーになる。画面上は正常に見えるため気づきにくい。

発生パターン

  1. 新規タスク作成時に一時ID(temp-xxx)で楽観的更新
  2. 直後にそのタスクの子データ(サブタスク)取得が走る
  3. /api/tasks/temp-xxx/subtasks へリクエスト → 400エラー

解決策

APIリクエストを発行する箇所で一時IDを除外する

const confirmedTaskIds = project.taskIds.filter(
  (id) => !id.startsWith('temp-')
);

ポイント:

  • 一時IDはUI即時反映用として残す
  • APIを呼ぶ直前でフィルタリングする
1
0
1

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?