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/nextjs-app-optimistic-updates/.gitignore similarity index 67% rename from examples/react/optimistic-updates-cache/.gitignore rename to examples/react/nextjs-app-optimistic-updates/.gitignore index 4673b022e53..b988ee9758f 100644 --- a/examples/react/optimistic-updates-cache/.gitignore +++ b/examples/react/nextjs-app-optimistic-updates/.gitignore @@ -8,20 +8,27 @@ # testing /coverage +# next.js +/.next/ +/out/ + # production /build -pnpm-lock.yaml -yarn.lock -package-lock.json - # misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local +*.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..1069ea411bf --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/README.md @@ -0,0 +1,61 @@ +# 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/data.ts b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts new file mode 100644 index 00000000000..21473d1b383 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/data.ts @@ -0,0 +1,28 @@ +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 new file mode 100644 index 00000000000..e6b030ad5c2 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/app/api/todos/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from 'next/server' +import { todos, getTodos, type Todo } from './data' + +export async function GET() { + return NextResponse.json(await getTodos()) +} + +export async function POST(request: Request) { + let body: unknown + try { + body = await request.json() + } catch { + return NextResponse.json({ error: 'text is required' }, { status: 400 }) + } + + 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)) + + if (Math.random() < 0.3) { + return NextResponse.json( + { error: 'Server error — please try again' }, + { status: 500 }, + ) + } + + const newTodo: Todo = { + id: crypto.randomUUID(), + 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..9b0ffe691fb --- /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/data' +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..28c758bbbf6 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/ApproachTabs.tsx @@ -0,0 +1,46 @@ +'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..b5002438cd8 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListCache.tsx @@ -0,0 +1,130 @@ +'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…)} +
    • + ))} +
    +
    + ) +} 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..3e2d310c2f3 --- /dev/null +++ b/examples/react/nextjs-app-optimistic-updates/components/TodoListUI.tsx @@ -0,0 +1,102 @@ +'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…) +
    • + )} +
    +
    + ) +} 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/optimistic-updates-ui/package.json b/examples/react/nextjs-app-optimistic-updates/package.json old mode 100755 new mode 100644 similarity index 81% rename from examples/react/optimistic-updates-ui/package.json rename to examples/react/nextjs-app-optimistic-updates/package.json index b11943b0a67..0019619fef5 --- a/examples/react/optimistic-updates-ui/package.json +++ b/examples/react/nextjs-app-optimistic-updates/package.json @@ -1,5 +1,5 @@ { - "name": "@tanstack/query-example-react-optimistic-updates-ui", + "name": "@tanstack/query-example-react-nextjs-app-optimistic-updates", "private": true, "type": "module", "scripts": { @@ -15,6 +15,7 @@ "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/optimistic-updates-ui/tsconfig.json b/examples/react/nextjs-app-optimistic-updates/tsconfig.json similarity index 54% rename from examples/react/optimistic-updates-ui/tsconfig.json rename to examples/react/nextjs-app-optimistic-updates/tsconfig.json index d03745c66b6..25e2693bc37 100644 --- a/examples/react/optimistic-updates-ui/tsconfig.json +++ b/examples/react/nextjs-app-optimistic-updates/tsconfig.json @@ -1,20 +1,35 @@ { "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "lib": ["dom", "es2015"], - "jsx": "react-jsx", "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, - "incremental": true + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } }, - "include": ["src"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".eslintrc.cjs", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules"] } 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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 8a17bc76eb4..13af5c1ee1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1064,6 +1064,37 @@ importers: specifier: 5.8.3 version: 5.8.3 + examples/react/nextjs-app-optimistic-updates: + 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/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': @@ -1163,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':