From 00a465254990507a05970854dcf2b71f3582940b Mon Sep 17 00:00:00 2001 From: Guilherme Rabelo Date: Sat, 27 Jun 2026 21:24:42 -0300 Subject: [PATCH 1/6] feat(examples): add Next.js App Router optimistic updates example --- .../.eslintrc.cjs | 9 ++ .../nextjs-app-optimistic-updates/.gitignore | 34 +++++ .../nextjs-app-optimistic-updates/README.md | 58 +++++++++ .../app/api/todos/route.ts | 45 +++++++ .../app/get-query-client.ts | 31 +++++ .../app/layout.tsx | 22 ++++ .../app/page.tsx | 27 ++++ .../app/providers.tsx | 16 +++ .../components/ApproachTabs.tsx | 36 ++++++ .../components/TodoListCache.tsx | 121 ++++++++++++++++++ .../components/TodoListUI.tsx | 96 ++++++++++++++ .../next.config.js | 6 + .../package.json | 23 ++++ .../tsconfig.json | 35 +++++ 14 files changed, 559 insertions(+) create mode 100644 examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs create mode 100644 examples/react/nextjs-app-optimistic-updates/.gitignore create mode 100644 examples/react/nextjs-app-optimistic-updates/README.md create mode 100644 examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts create mode 100644 examples/react/nextjs-app-optimistic-updates/app/get-query-client.ts create mode 100644 examples/react/nextjs-app-optimistic-updates/app/layout.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/app/page.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/app/providers.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx create mode 100644 examples/react/nextjs-app-optimistic-updates/next.config.js create mode 100644 examples/react/nextjs-app-optimistic-updates/package.json create mode 100644 examples/react/nextjs-app-optimistic-updates/tsconfig.json diff --git a/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs b/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs new file mode 100644 index 00000000000..cb40aee1b47 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs @@ -0,0 +1,9 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'], + settings: { + react: { + version: 'detect', + }, + }, +} diff --git a/examples/react/nextjs-app-optimistic-updates/.gitignore b/examples/react/nextjs-app-optimistic-updates/.gitignore new file mode 100644 index 00000000000..b988ee9758f --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo diff --git a/examples/react/nextjs-app-optimistic-updates/README.md b/examples/react/nextjs-app-optimistic-updates/README.md new file mode 100644 index 00000000000..89b24ce9dad --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/README.md @@ -0,0 +1,58 @@ +# TanStack Query — Next.js App Router Optimistic Updates + +This example demonstrates **optimistic updates** with TanStack Query v5 in a Next.js 14 App Router project. + +## What it shows + +A todo list where items appear in the UI immediately after submission — before the server confirms. The server randomly fails ~30% of the time so you can observe automatic rollback behaviour. + +Two approaches are shown side by side via a tab toggle: + +### Approach 1 — Via UI Variables (simpler) + +Render the pending item directly from `mutation.variables`. No cache touching required. On error, the pending item simply disappears and an error message is shown. + +```ts +const mutation = useMutation({ mutationFn: addTodo, onSettled: invalidate }) + +// In JSX: +{mutation.isPending &&
  • {mutation.variables}
  • } +``` + +**Best when:** the mutation input maps 1-to-1 to what you'd show while pending. + +### Approach 2 — Via Cache Manipulation (`onMutate` + rollback) + +`onMutate` cancels in-flight refetches, snapshots the current cache, and writes an optimistic item directly into the cache. `onError` restores the snapshot. + +```ts +const mutation = useMutation({ + mutationFn: addTodo, + onMutate: async (text) => { + await queryClient.cancelQueries({ queryKey: ['todos'] }) + const previousTodos = queryClient.getQueryData(['todos']) + queryClient.setQueryData(['todos'], (old = []) => [...old, optimistic]) + return { previousTodos } + }, + onError: (_err, _vars, context) => { + queryClient.setQueryData(['todos'], context?.previousTodos) + }, + onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), +}) +``` + +**Best when:** you need fine-grained control or the optimistic shape differs from `mutation.variables`. + +## Running the example + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). + +## Learn more + +- [TanStack Query — Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates) +- [Next.js App Router](https://nextjs.org/docs/app) diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts new file mode 100644 index 00000000000..da625480c16 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from 'next/server' + +export interface Todo { + id: string + text: string + createdAt: number +} + +export const todos: Array = [ + { id: crypto.randomUUID(), text: 'Buy groceries', createdAt: Date.now() - 3000 }, + { id: crypto.randomUUID(), text: 'Walk the dog', createdAt: Date.now() - 2000 }, + { id: crypto.randomUUID(), text: 'Read a book', createdAt: Date.now() - 1000 }, +] + +export async function getTodos(): Promise> { + await new Promise((resolve) => setTimeout(resolve, 200)) + return todos +} + +export async function GET() { + return NextResponse.json(await getTodos()) +} + +export async function POST(request: Request) { + const body = (await request.json()) as { text: string } + + await new Promise((resolve) => setTimeout(resolve, 500)) + + if (Math.random() < 0.3) { + return NextResponse.json( + { error: 'Server error — please try again' }, + { status: 500 }, + ) + } + + const newTodo: Todo = { + id: crypto.randomUUID(), + text: body.text, + createdAt: Date.now(), + } + + todos.push(newTodo) + + return NextResponse.json(newTodo, { status: 201 }) +} diff --git a/examples/react/nextjs-app-optimistic-updates/app/get-query-client.ts b/examples/react/nextjs-app-optimistic-updates/app/get-query-client.ts new file mode 100644 index 00000000000..1666beba77a --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/get-query-client.ts @@ -0,0 +1,31 @@ +import { + QueryClient, + defaultShouldDehydrateQuery, + environmentManager, +} from '@tanstack/react-query' + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + dehydrate: { + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + }, + }) +} + +let browserQueryClient: QueryClient | undefined = undefined + +export function getQueryClient() { + if (environmentManager.isServer()) { + return makeQueryClient() + } else { + if (!browserQueryClient) browserQueryClient = makeQueryClient() + return browserQueryClient + } +} diff --git a/examples/react/nextjs-app-optimistic-updates/app/layout.tsx b/examples/react/nextjs-app-optimistic-updates/app/layout.tsx new file mode 100644 index 00000000000..055162043cf --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/layout.tsx @@ -0,0 +1,22 @@ +import Providers from './providers' +import type React from 'react' +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'TanStack Query — Optimistic Updates', + description: 'Next.js App Router example with optimistic updates', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/app/page.tsx b/examples/react/nextjs-app-optimistic-updates/app/page.tsx new file mode 100644 index 00000000000..52310a3e325 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/page.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { HydrationBoundary, dehydrate } from '@tanstack/react-query' +import { getQueryClient } from '@/app/get-query-client' +import { getTodos } from '@/app/api/todos/route' +import ApproachTabs from '@/components/ApproachTabs' + +export default function Home() { + const queryClient = getQueryClient() + + void queryClient.prefetchQuery({ + queryKey: ['todos'], + queryFn: getTodos, + }) + + return ( +
    +

    Optimistic Updates with TanStack Query

    +

    + Add todos to see optimistic updates in action. The server randomly fails + ~30% of the time so you can observe automatic rollback. +

    + + + +
    + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/app/providers.tsx b/examples/react/nextjs-app-optimistic-updates/app/providers.tsx new file mode 100644 index 00000000000..f5098b4d0a6 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/providers.tsx @@ -0,0 +1,16 @@ +'use client' +import { QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { getQueryClient } from '@/app/get-query-client' +import type * as React from 'react' + +export default function Providers({ children }: { children: React.ReactNode }) { + const queryClient = getQueryClient() + + return ( + + {children} + + + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx b/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx new file mode 100644 index 00000000000..a9ed26e5a08 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx @@ -0,0 +1,36 @@ +'use client' + +import React, { useState } from 'react' +import TodoListUI from './TodoListUI' +import TodoListCache from './TodoListCache' + +type Tab = 'ui-variables' | 'cache' + +export default function ApproachTabs() { + const [activeTab, setActiveTab] = useState('ui-variables') + + const tabStyle = (tab: Tab): React.CSSProperties => ({ + padding: '0.5rem 1rem', + border: 'none', + borderBottom: activeTab === tab ? '2px solid #0070f3' : '2px solid transparent', + background: 'none', + cursor: 'pointer', + fontWeight: activeTab === tab ? 600 : 400, + color: activeTab === tab ? '#0070f3' : '#555', + }) + + return ( +
    +
    + + +
    + + {activeTab === 'ui-variables' ? : } +
    + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx b/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx new file mode 100644 index 00000000000..90ffb462661 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx @@ -0,0 +1,121 @@ +'use client' + +import React, { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import type { Todo } from '@/app/api/todos/route' + +interface MutationContext { + previousTodos: Array | undefined +} + +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 optimisticTodo: Todo = { + id: `optimistic-${Date.now()}`, + text, + createdAt: Date.now(), + } + + queryClient.setQueryData>(['todos'], (old = []) => [ + ...old, + optimisticTodo, + ]) + + return { previousTodos } + }, + onError: (_err, _text, context) => { + setLastError(_err.message) + if (context?.previousTodos !== undefined) { + queryClient.setQueryData(['todos'], context.previousTodos) + } + }, + 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…)} +
    • + ))} +
    +
    + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx b/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx new file mode 100644 index 00000000000..516ec04a44f --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx @@ -0,0 +1,96 @@ +'use client' + +import React, { useState } from 'react' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import type { Todo } from '@/app/api/todos/route' + +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…) +
    • + )} +
    +
    + ) +} diff --git a/examples/react/nextjs-app-optimistic-updates/next.config.js b/examples/react/nextjs-app-optimistic-updates/next.config.js new file mode 100644 index 00000000000..296d026f634 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/next.config.js @@ -0,0 +1,6 @@ +// @ts-check + +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +export default nextConfig diff --git a/examples/react/nextjs-app-optimistic-updates/package.json b/examples/react/nextjs-app-optimistic-updates/package.json new file mode 100644 index 00000000000..ba5252255de --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/package.json @@ -0,0 +1,23 @@ +{ + "name": "@tanstack/query-example-react-nextjs-app-optimistic-updates", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@tanstack/react-query": "^5.101.2", + "@tanstack/react-query-devtools": "^5.101.2", + "next": "^16.0.7", + "react": "^19.2.1", + "react-dom": "^19.2.1" + }, + "devDependencies": { + "@types/node": "26.0.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "typescript": "5.8.3" + } +} diff --git a/examples/react/nextjs-app-optimistic-updates/tsconfig.json b/examples/react/nextjs-app-optimistic-updates/tsconfig.json new file mode 100644 index 00000000000..25e2693bc37 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".eslintrc.cjs", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +} From 7ad24c16497c88d97951c7e25924c9d8e372b5f1 Mon Sep 17 00:00:00 2001 From: Guilherme Rabelo Date: Sat, 27 Jun 2026 22:37:51 -0300 Subject: [PATCH 2/6] refactor(examples/nextjs-app-optimistic-updates): apply post-review corrections --- .../app/api/todos/data.ts | 16 +++++++++ .../app/api/todos/route.ts | 34 ++++++++----------- .../app/page.tsx | 2 +- .../components/TodoListCache.tsx | 12 +++++-- .../components/TodoListUI.tsx | 2 +- pnpm-lock.yaml | 31 +++++++++++++++++ 6 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts new file mode 100644 index 00000000000..32acd79356f --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts @@ -0,0 +1,16 @@ +export interface Todo { + id: string + text: string + createdAt: number +} + +export const todos: Array = [ + { id: crypto.randomUUID(), text: 'Buy groceries', createdAt: Date.now() - 3000 }, + { id: crypto.randomUUID(), text: 'Walk the dog', createdAt: Date.now() - 2000 }, + { id: crypto.randomUUID(), text: 'Read a book', createdAt: Date.now() - 1000 }, +] + +export async function getTodos(): Promise> { + await new Promise((resolve) => setTimeout(resolve, 200)) + return todos +} diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts index da625480c16..09867467b60 100644 --- a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts @@ -1,28 +1,24 @@ import { NextResponse } from 'next/server' - -export interface Todo { - id: string - text: string - createdAt: number -} - -export const todos: Array = [ - { id: crypto.randomUUID(), text: 'Buy groceries', createdAt: Date.now() - 3000 }, - { id: crypto.randomUUID(), text: 'Walk the dog', createdAt: Date.now() - 2000 }, - { id: crypto.randomUUID(), text: 'Read a book', createdAt: Date.now() - 1000 }, -] - -export async function getTodos(): Promise> { - await new Promise((resolve) => setTimeout(resolve, 200)) - return todos -} +import { todos, getTodos, type Todo } from './data' export async function GET() { return NextResponse.json(await getTodos()) } export async function POST(request: Request) { - const body = (await request.json()) as { text: string } + const body = (await request.json()) as unknown + + const text = + body !== null && + typeof body === 'object' && + 'text' in body && + typeof (body as { text: unknown }).text === 'string' + ? ((body as { text: string }).text.trim()) + : '' + + if (!text) { + return NextResponse.json({ error: 'text is required' }, { status: 400 }) + } await new Promise((resolve) => setTimeout(resolve, 500)) @@ -35,7 +31,7 @@ export async function POST(request: Request) { const newTodo: Todo = { id: crypto.randomUUID(), - text: body.text, + text, createdAt: Date.now(), } diff --git a/examples/react/nextjs-app-optimistic-updates/app/page.tsx b/examples/react/nextjs-app-optimistic-updates/app/page.tsx index 52310a3e325..9b0ffe691fb 100644 --- a/examples/react/nextjs-app-optimistic-updates/app/page.tsx +++ b/examples/react/nextjs-app-optimistic-updates/app/page.tsx @@ -1,7 +1,7 @@ import React from 'react' import { HydrationBoundary, dehydrate } from '@tanstack/react-query' import { getQueryClient } from '@/app/get-query-client' -import { getTodos } from '@/app/api/todos/route' +import { getTodos } from '@/app/api/todos/data' import ApproachTabs from '@/components/ApproachTabs' export default function Home() { diff --git a/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx b/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx index 90ffb462661..674c7a0f577 100644 --- a/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx @@ -2,10 +2,11 @@ import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import type { Todo } from '@/app/api/todos/route' +import type { Todo } from '@/app/api/todos/data' interface MutationContext { previousTodos: Array | undefined + optimisticId: string } async function fetchTodos(): Promise> { @@ -45,8 +46,9 @@ export default function TodoListCache() { const previousTodos = queryClient.getQueryData>(['todos']) + const optimisticId = `optimistic-${Date.now()}` const optimisticTodo: Todo = { - id: `optimistic-${Date.now()}`, + id: optimisticId, text, createdAt: Date.now(), } @@ -56,12 +58,16 @@ export default function TodoListCache() { optimisticTodo, ]) - return { previousTodos } + 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: () => { diff --git a/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx b/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx index 516ec04a44f..be867ced180 100644 --- a/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' -import type { Todo } from '@/app/api/todos/route' +import type { Todo } from '@/app/api/todos/data' async function fetchTodos(): Promise> { const res = await fetch('/api/todos') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24698d4fd93..c47960c1904 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1070,6 +1070,37 @@ importers: specifier: 5.8.3 version: 5.8.3 + examples/react/nextjs-app-optimistic-updates: + dependencies: + '@tanstack/react-query': + specifier: ^5.101.2 + version: link:../../../packages/react-query + '@tanstack/react-query-devtools': + specifier: ^5.101.2 + version: link:../../../packages/react-query-devtools + next: + specifier: ^16.0.7 + version: 16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0) + react: + specifier: ^19.2.1 + version: 19.2.4 + react-dom: + specifier: ^19.2.1 + version: 19.2.4(react@19.2.4) + devDependencies: + '@types/node': + specifier: ^22.15.3 + version: 22.19.15 + '@types/react': + specifier: ^19.2.7 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: 5.8.3 + version: 5.8.3 + examples/react/nextjs-app-prefetching: dependencies: '@tanstack/react-query': From 083bcdada748675e9c231cad0bf298fbea81df91 Mon Sep 17 00:00:00 2001 From: Guilherme Rabelo Date: Sat, 27 Jun 2026 23:02:13 -0300 Subject: [PATCH 3/6] refactor(examples/nextjs-app-optimistic-updates): apply post-review corrections --- .../react/nextjs-app-optimistic-updates/.eslintrc.cjs | 9 --------- .../nextjs-app-optimistic-updates/app/api/todos/route.ts | 7 ++++++- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs diff --git a/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs b/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs deleted file mode 100644 index cb40aee1b47..00000000000 --- a/examples/react/nextjs-app-optimistic-updates/.eslintrc.cjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - extends: ['plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'], - settings: { - react: { - version: 'detect', - }, - }, -} diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts index 09867467b60..8a91e4e7207 100644 --- a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts @@ -6,7 +6,12 @@ export async function GET() { } export async function POST(request: Request) { - const body = (await request.json()) as unknown + let body: unknown + try { + body = await request.json() + } catch { + return NextResponse.json({ error: 'text is required' }, { status: 400 }) + } const text = body !== null && From 17f9bbfdfc6cc5833fb013902f86cbd261960a26 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 18 Aug 2026 09:44:39 +0200 Subject: [PATCH 4/6] update deps --- examples/react/nextjs-app-optimistic-updates/package.json | 6 +++--- pnpm-lock.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/react/nextjs-app-optimistic-updates/package.json b/examples/react/nextjs-app-optimistic-updates/package.json index ba5252255de..0019619fef5 100644 --- a/examples/react/nextjs-app-optimistic-updates/package.json +++ b/examples/react/nextjs-app-optimistic-updates/package.json @@ -3,13 +3,13 @@ "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build", "start": "next start" }, "dependencies": { - "@tanstack/react-query": "^5.101.2", - "@tanstack/react-query-devtools": "^5.101.2", + "@tanstack/react-query": "^5.101.4", + "@tanstack/react-query-devtools": "^5.101.4", "next": "^16.0.7", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56fa5d840fb..d4c0b13c67f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1067,10 +1067,10 @@ importers: examples/react/nextjs-app-optimistic-updates: dependencies: '@tanstack/react-query': - specifier: ^5.101.2 + specifier: ^5.101.4 version: link:../../../packages/react-query '@tanstack/react-query-devtools': - specifier: ^5.101.2 + specifier: ^5.101.4 version: link:../../../packages/react-query-devtools next: specifier: ^16.0.7 From 8f010b7d46311666b54398d2ac397b157fd02bcd Mon Sep 17 00:00:00 2001 From: TkDodo Date: Tue, 18 Aug 2026 09:46:54 +0200 Subject: [PATCH 5/6] ref: remove other optimistic update examples --- docs/config.json | 8 +- .../react/optimistic-updates-cache/.gitignore | 27 ---- .../react/optimistic-updates-cache/README.md | 6 - .../optimistic-updates-cache/next.config.js | 13 -- .../optimistic-updates-cache/package.json | 22 --- .../src/pages/api/data.ts | 31 ---- .../src/pages/index.tsx | 138 ------------------ .../optimistic-updates-cache/tsconfig.json | 20 --- .../react/optimistic-updates-ui/.gitignore | 27 ---- .../react/optimistic-updates-ui/README.md | 6 - .../optimistic-updates-ui/next.config.js | 13 -- .../react/optimistic-updates-ui/package.json | 22 --- .../src/pages/api/data.ts | 31 ---- .../optimistic-updates-ui/src/pages/index.tsx | 117 --------------- .../react/optimistic-updates-ui/tsconfig.json | 20 --- pnpm-lock.yaml | 56 ------- 16 files changed, 2 insertions(+), 555 deletions(-) delete mode 100644 examples/react/optimistic-updates-cache/.gitignore delete mode 100644 examples/react/optimistic-updates-cache/README.md delete mode 100644 examples/react/optimistic-updates-cache/next.config.js delete mode 100755 examples/react/optimistic-updates-cache/package.json delete mode 100644 examples/react/optimistic-updates-cache/src/pages/api/data.ts delete mode 100644 examples/react/optimistic-updates-cache/src/pages/index.tsx delete mode 100644 examples/react/optimistic-updates-cache/tsconfig.json delete mode 100644 examples/react/optimistic-updates-ui/.gitignore delete mode 100644 examples/react/optimistic-updates-ui/README.md delete mode 100644 examples/react/optimistic-updates-ui/next.config.js delete mode 100755 examples/react/optimistic-updates-ui/package.json delete mode 100644 examples/react/optimistic-updates-ui/src/pages/api/data.ts delete mode 100644 examples/react/optimistic-updates-ui/src/pages/index.tsx delete mode 100644 examples/react/optimistic-updates-ui/tsconfig.json diff --git a/docs/config.json b/docs/config.json index 21737a41321..5e056704257 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1555,12 +1555,8 @@ "to": "framework/react/examples/auto-refetching" }, { - "label": "Optimistic Updates (UI)", - "to": "framework/react/examples/optimistic-updates-ui" - }, - { - "label": "Optimistic Updates (Cache)", - "to": "framework/react/examples/optimistic-updates-cache" + "label": "Optimistic Updates", + "to": "framework/react/examples/nextjs-app-optimistic-updates" }, { "label": "Pagination", diff --git a/examples/react/optimistic-updates-cache/.gitignore b/examples/react/optimistic-updates-cache/.gitignore deleted file mode 100644 index 4673b022e53..00000000000 --- a/examples/react/optimistic-updates-cache/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -pnpm-lock.yaml -yarn.lock -package-lock.json - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/examples/react/optimistic-updates-cache/README.md b/examples/react/optimistic-updates-cache/README.md deleted file mode 100644 index 1cf88926527..00000000000 --- a/examples/react/optimistic-updates-cache/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Example - -To run this example: - -- `npm install` -- `npm run dev` diff --git a/examples/react/optimistic-updates-cache/next.config.js b/examples/react/optimistic-updates-cache/next.config.js deleted file mode 100644 index 033754658c1..00000000000 --- a/examples/react/optimistic-updates-cache/next.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -/** @type {import('next').NextConfig} */ -const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, -} - -export default nextConfig diff --git a/examples/react/optimistic-updates-cache/package.json b/examples/react/optimistic-updates-cache/package.json deleted file mode 100755 index 6c4d32eff2c..00000000000 --- a/examples/react/optimistic-updates-cache/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@tanstack/query-example-react-optimistic-updates-cache", - "private": true, - "type": "module", - "scripts": { - "dev": "next dev --webpack", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "@tanstack/react-query": "^5.101.4", - "@tanstack/react-query-devtools": "^5.101.4", - "next": "^16.0.7", - "react": "^19.2.1", - "react-dom": "^19.2.1" - }, - "devDependencies": { - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "typescript": "5.8.3" - } -} diff --git a/examples/react/optimistic-updates-cache/src/pages/api/data.ts b/examples/react/optimistic-updates-cache/src/pages/api/data.ts deleted file mode 100644 index eabd6f5f7d3..00000000000 --- a/examples/react/optimistic-updates-cache/src/pages/api/data.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next' - -type Todo = { id: string; text: string } - -const items: Array = [] - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await new Promise((r) => setTimeout(r, 1000)) - - if (req.method === 'POST') { - const text: string = req.body.text - - // sometimes it will fail, this will cause a regression on the UI - - if (Math.random() > 0.7) { - res.status(500) - res.json({ message: 'Could not add item!' }) - return - } - - const newTodo = { id: Math.random().toString(), text: text.toUpperCase() } - items.push(newTodo) - res.json(newTodo) - return - } else { - res.json({ - ts: Date.now(), - items, - }) - } -} diff --git a/examples/react/optimistic-updates-cache/src/pages/index.tsx b/examples/react/optimistic-updates-cache/src/pages/index.tsx deleted file mode 100644 index be8a4fcd893..00000000000 --- a/examples/react/optimistic-updates-cache/src/pages/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import * as React from 'react' - -import { - QueryClient, - QueryClientProvider, - queryOptions, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - -const client = new QueryClient() - -type Todos = { - items: ReadonlyArray<{ - id: string - text: string - }> - ts: number -} - -async function fetchTodos({ signal }: { signal: AbortSignal }): Promise { - const response = await fetch('/api/data', { signal }) - return await response.json() -} - -const todoListOptions = queryOptions({ - queryKey: ['todos'], - queryFn: fetchTodos, -}) - -function Example() { - const queryClient = useQueryClient() - const [text, setText] = React.useState('') - const { isFetching, ...queryInfo } = useQuery(todoListOptions) - - const addTodoMutation = useMutation({ - mutationFn: async (newTodo: string) => { - const response = await fetch('/api/data', { - method: 'POST', - body: JSON.stringify({ text: newTodo }), - headers: { 'Content-Type': 'application/json' }, - }) - return await response.json() - }, - // When mutate is called: - onMutate: async (newTodo, context) => { - setText('') - // Cancel any outgoing refetch - // (so they don't overwrite our optimistic update) - await context.client.cancelQueries(todoListOptions) - - // Snapshot the previous value - const previousTodos = context.client.getQueryData( - todoListOptions.queryKey, - ) - - // Optimistically update to the new value - if (previousTodos) { - context.client.setQueryData(todoListOptions.queryKey, { - ...previousTodos, - items: [ - ...previousTodos.items, - { id: Math.random().toString(), text: newTodo }, - ], - }) - } - - return { previousTodos } - }, - // If the mutation fails, - // use the result returned from onMutate to roll back - onError: (err, variables, onMutateResult, context) => { - if (onMutateResult?.previousTodos) { - context.client.setQueryData( - ['todos'], - onMutateResult.previousTodos, - ) - } - }, - // Always refetch after error or success: - onSettled: (data, error, variables, onMutateResult, context) => - context.client.invalidateQueries({ queryKey: ['todos'] }), - }) - - return ( -
    -

    - In this example, new items can be created using a mutation. The new item - will be optimistically added to the list in hopes that the server - accepts the item. If it does, the list is refetched with the true items - from the list. Every now and then, the mutation may fail though. When - that happens, the previous list of items is restored and the list is - again refetched from the server. -

    -
    { - e.preventDefault() - addTodoMutation.mutate(text) - }} - > - setText(event.target.value)} - value={text} - /> - -
    -
    - {queryInfo.isSuccess && ( - <> -
    - {/* The type of queryInfo.data will be narrowed because we check for isSuccess first */} - Updated At: {new Date(queryInfo.data.ts).toLocaleTimeString()} -
    -
      - {queryInfo.data.items.map((todo) => ( -
    • {todo.text}
    • - ))} -
    - {isFetching &&
    Updating in background...
    } - - )} - {queryInfo.isLoading && 'Loading'} - {queryInfo.error instanceof Error && queryInfo.error.message} -
    - ) -} - -export default function App() { - return ( - - - - - ) -} diff --git a/examples/react/optimistic-updates-cache/tsconfig.json b/examples/react/optimistic-updates-cache/tsconfig.json deleted file mode 100644 index f8c94d3a332..00000000000 --- a/examples/react/optimistic-updates-cache/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "lib": ["DOM", "ES2015"], - "jsx": "react-jsx", - "target": "ES5", - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "incremental": true - }, - "include": ["src"], - "exclude": ["node_modules"] -} diff --git a/examples/react/optimistic-updates-ui/.gitignore b/examples/react/optimistic-updates-ui/.gitignore deleted file mode 100644 index 4673b022e53..00000000000 --- a/examples/react/optimistic-updates-ui/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -pnpm-lock.yaml -yarn.lock -package-lock.json - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/examples/react/optimistic-updates-ui/README.md b/examples/react/optimistic-updates-ui/README.md deleted file mode 100644 index 1cf88926527..00000000000 --- a/examples/react/optimistic-updates-ui/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Example - -To run this example: - -- `npm install` -- `npm run dev` diff --git a/examples/react/optimistic-updates-ui/next.config.js b/examples/react/optimistic-updates-ui/next.config.js deleted file mode 100644 index 033754658c1..00000000000 --- a/examples/react/optimistic-updates-ui/next.config.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -/** @type {import('next').NextConfig} */ -const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, -} - -export default nextConfig diff --git a/examples/react/optimistic-updates-ui/package.json b/examples/react/optimistic-updates-ui/package.json deleted file mode 100755 index b11943b0a67..00000000000 --- a/examples/react/optimistic-updates-ui/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "@tanstack/query-example-react-optimistic-updates-ui", - "private": true, - "type": "module", - "scripts": { - "dev": "next dev --webpack", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "@tanstack/react-query": "^5.101.4", - "@tanstack/react-query-devtools": "^5.101.4", - "next": "^16.0.7", - "react": "^19.2.1", - "react-dom": "^19.2.1" - }, - "devDependencies": { - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "typescript": "5.8.3" - } -} diff --git a/examples/react/optimistic-updates-ui/src/pages/api/data.ts b/examples/react/optimistic-updates-ui/src/pages/api/data.ts deleted file mode 100644 index eabd6f5f7d3..00000000000 --- a/examples/react/optimistic-updates-ui/src/pages/api/data.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from 'next' - -type Todo = { id: string; text: string } - -const items: Array = [] - -export default async (req: NextApiRequest, res: NextApiResponse) => { - await new Promise((r) => setTimeout(r, 1000)) - - if (req.method === 'POST') { - const text: string = req.body.text - - // sometimes it will fail, this will cause a regression on the UI - - if (Math.random() > 0.7) { - res.status(500) - res.json({ message: 'Could not add item!' }) - return - } - - const newTodo = { id: Math.random().toString(), text: text.toUpperCase() } - items.push(newTodo) - res.json(newTodo) - return - } else { - res.json({ - ts: Date.now(), - items, - }) - } -} diff --git a/examples/react/optimistic-updates-ui/src/pages/index.tsx b/examples/react/optimistic-updates-ui/src/pages/index.tsx deleted file mode 100644 index 7e4a8e4b128..00000000000 --- a/examples/react/optimistic-updates-ui/src/pages/index.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react' -import { - QueryClient, - QueryClientProvider, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - -const client = new QueryClient() - -type Todos = { - items: ReadonlyArray<{ - id: string - text: string - }> - ts: number -} - -async function fetchTodos(): Promise { - const response = await fetch('/api/data') - return await response.json() -} - -function useTodos() { - return useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) -} - -function Example() { - const queryClient = useQueryClient() - const [text, setText] = React.useState('') - const todoQuery = useTodos() - - const addTodoMutation = useMutation({ - mutationFn: async (newTodo: string) => { - const response = await fetch('/api/data', { - method: 'POST', - body: JSON.stringify({ text: newTodo }), - headers: { 'Content-Type': 'application/json' }, - }) - if (!response.ok) { - throw new Error('Something went wrong.') - } - return await response.json() - }, - onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), - }) - - return ( -
    -

    - In this example, new items can be created using a mutation. The new item - will be optimistically added to the list in hopes that the server - accepts the item. If it does, the list is refetched with the true items - from the list. Every now and then, the mutation may fail though. When - that happens, the previous list of items is restored and the list is - again refetched from the server. -

    -
    { - e.preventDefault() - setText('') - addTodoMutation.mutate(text) - }} - > - setText(event.target.value)} - value={text} - /> - -
    -
    - {todoQuery.isSuccess && ( - <> -
    - {/* The type of queryInfo.data will be narrowed because we check for isSuccess first */} - Updated At: {new Date(todoQuery.data.ts).toLocaleTimeString()} -
    -
      - {todoQuery.data.items.map((todo) => ( -
    • {todo.text}
    • - ))} - {addTodoMutation.isPending && ( -
    • {addTodoMutation.variables}
    • - )} - {addTodoMutation.isError && ( -
    • - {addTodoMutation.variables} - -
    • - )} -
    - {todoQuery.isFetching &&
    Updating in background...
    } - - )} - {todoQuery.isPending && 'Loading'} - {todoQuery.error instanceof Error && todoQuery.error.message} -
    - ) -} - -export default function App() { - return ( - - - - - ) -} diff --git a/examples/react/optimistic-updates-ui/tsconfig.json b/examples/react/optimistic-updates-ui/tsconfig.json deleted file mode 100644 index d03745c66b6..00000000000 --- a/examples/react/optimistic-updates-ui/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "lib": ["dom", "es2015"], - "jsx": "react-jsx", - "target": "ES5", - "allowJs": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "incremental": true - }, - "include": ["src"], - "exclude": ["node_modules"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4c0b13c67f..13af5c1ee1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1194,62 +1194,6 @@ importers: specifier: ^6.4.1 version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - examples/react/optimistic-updates-cache: - dependencies: - '@tanstack/react-query': - specifier: ^5.101.4 - version: link:../../../packages/react-query - '@tanstack/react-query-devtools': - specifier: ^5.101.4 - version: link:../../../packages/react-query-devtools - next: - specifier: ^16.0.7 - version: 16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0) - react: - specifier: ^19.2.1 - version: 19.2.4 - react-dom: - specifier: ^19.2.1 - version: 19.2.4(react@19.2.4) - devDependencies: - '@types/react': - specifier: ^19.2.7 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - typescript: - specifier: 5.8.3 - version: 5.8.3 - - examples/react/optimistic-updates-ui: - dependencies: - '@tanstack/react-query': - specifier: ^5.101.4 - version: link:../../../packages/react-query - '@tanstack/react-query-devtools': - specifier: ^5.101.4 - version: link:../../../packages/react-query-devtools - next: - specifier: ^16.0.7 - version: 16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0) - react: - specifier: ^19.2.1 - version: 19.2.4 - react-dom: - specifier: ^19.2.1 - version: 19.2.4(react@19.2.4) - devDependencies: - '@types/react': - specifier: ^19.2.7 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - typescript: - specifier: 5.8.3 - version: 5.8.3 - examples/react/pagination: dependencies: '@tanstack/react-query': From f40a0b5879009bf70f12fd8bdb782c67be9cef65 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 07:48:39 +0000 Subject: [PATCH 6/6] ci: apply automated fixes --- .../nextjs-app-optimistic-updates/README.md | 5 ++++- .../app/api/todos/data.ts | 18 +++++++++++++++--- .../app/api/todos/route.ts | 2 +- .../components/ApproachTabs.tsx | 16 +++++++++++++--- .../components/TodoListCache.tsx | 11 +++++++---- .../components/TodoListUI.tsx | 10 ++++++++-- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/examples/react/nextjs-app-optimistic-updates/README.md b/examples/react/nextjs-app-optimistic-updates/README.md index 89b24ce9dad..1069ea411bf 100644 --- a/examples/react/nextjs-app-optimistic-updates/README.md +++ b/examples/react/nextjs-app-optimistic-updates/README.md @@ -31,7 +31,10 @@ const mutation = useMutation({ onMutate: async (text) => { await queryClient.cancelQueries({ queryKey: ['todos'] }) const previousTodos = queryClient.getQueryData(['todos']) - queryClient.setQueryData(['todos'], (old = []) => [...old, optimistic]) + queryClient.setQueryData(['todos'], (old = []) => [ + ...old, + optimistic, + ]) return { previousTodos } }, onError: (_err, _vars, context) => { diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts index 32acd79356f..21473d1b383 100644 --- a/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts @@ -5,9 +5,21 @@ export interface Todo { } export const todos: Array = [ - { id: crypto.randomUUID(), text: 'Buy groceries', createdAt: Date.now() - 3000 }, - { id: crypto.randomUUID(), text: 'Walk the dog', createdAt: Date.now() - 2000 }, - { id: crypto.randomUUID(), text: 'Read a book', createdAt: Date.now() - 1000 }, + { + id: crypto.randomUUID(), + text: 'Buy groceries', + createdAt: Date.now() - 3000, + }, + { + id: crypto.randomUUID(), + text: 'Walk the dog', + createdAt: Date.now() - 2000, + }, + { + id: crypto.randomUUID(), + text: 'Read a book', + createdAt: Date.now() - 1000, + }, ] export async function getTodos(): Promise> { diff --git a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts index 8a91e4e7207..e6b030ad5c2 100644 --- a/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts @@ -18,7 +18,7 @@ export async function POST(request: Request) { typeof body === 'object' && 'text' in body && typeof (body as { text: unknown }).text === 'string' - ? ((body as { text: string }).text.trim()) + ? (body as { text: string }).text.trim() : '' if (!text) { diff --git a/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx b/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx index a9ed26e5a08..28c758bbbf6 100644 --- a/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx +++ b/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx @@ -12,7 +12,8 @@ export default function ApproachTabs() { const tabStyle = (tab: Tab): React.CSSProperties => ({ padding: '0.5rem 1rem', border: 'none', - borderBottom: activeTab === tab ? '2px solid #0070f3' : '2px solid transparent', + borderBottom: + activeTab === tab ? '2px solid #0070f3' : '2px solid transparent', background: 'none', cursor: 'pointer', fontWeight: activeTab === tab ? 600 : 400, @@ -21,8 +22,17 @@ export default function ApproachTabs() { return (
    -
    -