'use client' import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import type { Todo } from '@/app/api/todos/data' 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 TodoListUI() { const queryClient = useQueryClient() const [inputValue, setInputValue] = useState('') const { data: todos = [] } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) const addTodoMutation = useMutation({ mutationFn: addTodo, 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 1 — via UI variables: The pending item is rendered directly from mutation.variables. No cache manipulation needed. On error the pending item simply disappears.

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

{addTodoMutation.error.message}

)}
    {todos.map((todo) => (
  • {todo.text}
  • ))} {addTodoMutation.isPending && (
  • {addTodoMutation.variables} (saving…)
  • )}
) }