'use client' import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import type { Todo } from '@/app/api/todos/data' interface MutationContext { previousTodos: Array | undefined optimisticId: string } async function fetchTodos(): Promise> { const res = await fetch('/api/todos') if (!res.ok) throw new Error('Failed to fetch todos') return res.json() } async function addTodo(text: string): Promise { const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }), }) if (!res.ok) { const err = (await res.json()) as { error: string } throw new Error(err.error) } return res.json() } export default function TodoListCache() { const queryClient = useQueryClient() const [inputValue, setInputValue] = useState('') const [lastError, setLastError] = useState(null) const { data: todos = [] } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) const addTodoMutation = useMutation({ mutationFn: addTodo, onMutate: async (text) => { setLastError(null) await queryClient.cancelQueries({ queryKey: ['todos'] }) const previousTodos = queryClient.getQueryData>(['todos']) const optimisticId = `optimistic-${Date.now()}` const optimisticTodo: Todo = { id: optimisticId, text, createdAt: Date.now(), } queryClient.setQueryData>(['todos'], (old = []) => [ ...old, optimisticTodo, ]) return { previousTodos, optimisticId } }, onError: (_err, _text, context) => { setLastError(_err.message) if (context?.previousTodos !== undefined) { queryClient.setQueryData(['todos'], context.previousTodos) } else if (context?.optimisticId) { queryClient.setQueryData>(['todos'], (old = []) => old.filter((todo) => todo.id !== context.optimisticId), ) } }, onSettled: () => { void queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) function handleSubmit(e: React.FormEvent) { e.preventDefault() const text = inputValue.trim() if (!text) return addTodoMutation.mutate(text) setInputValue('') } return (

Approach 2 — via cache manipulation:{' '} onMutate snapshots the cache and writes the optimistic item in. onError restores the snapshot on failure.

setInputValue(e.target.value)} placeholder="New todo…" style={{ flex: 1, padding: '0.4rem 0.6rem', fontSize: '1rem' }} />
{lastError && (

{lastError}

)}
    {todos.map((todo) => (
  • {todo.text} {todo.id.startsWith('optimistic-') && (saving…)}
  • ))}
) }