Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 4.2k
docs(example): batching#11008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
docs(example): batching #11008
Changes from all commits
87b017c1bb965a63c5eb54d408daabec47dcef22bbac1bbe20957544File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # 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* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Example | ||
| To run this example: | ||
| - `npm install` | ||
| - `npm run dev` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { tanstackConfig } from '@tanstack/eslint-config' | ||
| import pluginQuery from '@tanstack/eslint-plugin-query' | ||
| import pluginReact from '@eslint-react/eslint-plugin' | ||
| export default [ | ||
| ...tanstackConfig, | ||
| ...pluginQuery.configs['flat/recommended'], | ||
| pluginReact.configs.recommended, | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,16 @@ | ||||||
| <!doctype html> | ||||||
| <html lang="en"> | ||||||
| <head> | ||||||
| <meta charset="utf-8" /> | ||||||
| <link rel="shortcut icon" type="image/svg+xml" href="/emblem-light.svg" /> | ||||||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||||
| <meta name="theme-color" content="#000000" /> | ||||||
| <title>TanStack Query React Basic Example App</title> | ||||||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Update the document title to match the batching example. Line 9 still says “Basic Example App”, which looks copied from another example and makes the browser tab misleading for this demo. Suggested fix- <title>TanStack Query React Basic Example App</title>+ <title>TanStack Query React Batching Example App</title>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||
| </head> | ||||||
| <body> | ||||||
| <noscript>You need to enable JavaScript to run this app.</noscript> | ||||||
| <div id="root"></div> | ||||||
| <script type="module" src="/src/index.tsx"></script> | ||||||
| </body> | ||||||
| </html> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "name": "@tanstack/query-example-react-batching", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "test:eslint": "eslint ./src" | ||
| }, | ||
| "dependencies": { | ||
| "@tanstack/react-pacer": "^0.22.1", | ||
| "@tanstack/react-query": "^5.101.4", | ||
| "@tanstack/react-query-devtools": "^5.101.4", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@tanstack/eslint-plugin-query": "^5.101.4", | ||
| "@types/react": "^18.2.79", | ||
| "@types/react-dom": "^18.2.25", | ||
| "@vitejs/plugin-react": "^4.3.4", | ||
| "typescript": "5.8.3", | ||
| "vite": "^6.4.1" | ||
| }, | ||
| "nx": { | ||
| "targets": { | ||
| "test:eslint": { | ||
| "dependsOn": [ | ||
| "^build" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { AsyncBatcher } from '@tanstack/react-pacer/async-batcher' | ||
| interface CharacterT { | ||
| id: number | ||
| name: string | ||
| status: string | ||
| species: string | ||
| type: string | ||
| gender: string | ||
| image: string | ||
| } | ||
| type CharacterRequest = { | ||
| deferred: PromiseWithResolvers<CharacterT | null> | ||
| id: number | ||
| } | ||
| // fetch function that returns characters by id | ||
| const fetchCharacters = async (ids: ReadonlyArray<number>) => { | ||
| console.log(`Fetching characters: ${ids.join(', ')}`) | ||
| const res = await fetch( | ||
| `https://rickandmortyapi.com/api/character/[${ids.join(',')}]`, | ||
| ) | ||
| const characters = (await res.json()) as Array<CharacterT> | CharacterT | ||
| const characterMap = new Map<number, CharacterT>() | ||
| for (const character of Array.isArray(characters) | ||
| ? characters | ||
| : [characters]) { | ||
| characterMap.set(character.id, character) | ||
| } | ||
| return characterMap | ||
| } | ||
| const characterBatcher = new AsyncBatcher<CharacterRequest>( | ||
| (requests) => fetchCharacters([...new Set(requests.map(({ id }) => id))]), | ||
| { | ||
| wait: 0, | ||
| onSuccess: (charactersById, requests) => { | ||
| for (const { deferred, id } of requests) { | ||
| deferred.resolve(charactersById.get(id) ?? null) | ||
| } | ||
| }, | ||
| onError: (error, requests) => { | ||
| for (const { deferred } of requests) { | ||
| deferred.reject(error) | ||
| } | ||
| }, | ||
| }, | ||
| ) | ||
| export const loadCharacter = (id: number) => { | ||
| const deferred = Promise.withResolvers<CharacterT | null>() | ||
| characterBatcher.addItem({ deferred, id }) | ||
| return deferred.promise | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ReactDOM from 'react-dom/client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QueryClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QueryClientProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryOptions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useQueries, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@tanstack/react-query' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ReactQueryDevtools } from '@tanstack/react-query-devtools' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loadCharacter } from './character-loader' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const queryClient = new QueryClient({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultOptions: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queries: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retry: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const characterQueryOptions = (id: number) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryOptions({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryKey: ['character', id], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queryFn: () => loadCharacter(id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| staleTime: 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Characters({ ids }: { ids: Array<number> }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const characters = useQueries({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| queries: ids.map((id) => characterQueryOptions(id)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return characters.map((character, index) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (character.status === 'pending') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'Loading...' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (character.status === 'error') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'Error...' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (character.data === null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'Not found ...' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div key={String(index)} style={{ display: 'flex' }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| width={100} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| height={100} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alt={character.data.image} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| src={character.data.image} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div style={{ paddingLeft: 10, textAlign: 'left' }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h5 style={{ marginTop: 0, marginBottom: 5 }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {character.data.name} (#{character.data.id}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </h5> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p style={{ marginTop: 0, marginBottom: 2 }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Status: {character.status} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p style={{ marginTop: 0, marginBottom: 2 }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Species: {character.data.species} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p style={{ marginTop: 0, marginBottom: 2 }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Gender: {character.data.gender} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function App() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [ids, setIds] = useState<Array<number>>([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [idsToLoad, setIdsToLoad] = useState<Array<number>>([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="App"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h2>Character fetcher</h2> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label style={{ display: 'block' }}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Character ids (numberic, comma seprated) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| style={{ display: 'inline-block' }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="ids" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = event.target.value.replace(' ', '') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ids = text.split(',').map((id) => parseInt(id, 10)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allValid = ids.reduce( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (valid, id) => valid && !isNaN(id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (allValid) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIds(ids) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError('Invalid.. only use numbers and comma') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {error ? <h3 style={{ color: 'red' }}>{error}</h3> : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIdsToLoad(ids) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+93
to
+108
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Clear or block stale IDs after validation fails. When parsing fails, Suggested fix if (allValid) {
setIds(ids)
setError('')
} else {
+ setIds([])
setError('Invalid.. only use numbers and comma')
}
@@
<button
type="button"
+ disabled={!!error || ids.length === 0}
onClick={() => {
setIdsToLoad(ids)
}}
>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Load characters | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h4 style={{ textAlign: 'left' }}>Characters:</h4> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Characters ids={idsToLoad} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Root() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <QueryClientProvider client={queryClient}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <App /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ReactQueryDevtools initialIsOpen /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </QueryClientProvider> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rootElement = document.getElementById('root') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rootElement) throw new Error('Missing #root element') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ReactDOM.createRoot(rootElement).render(<Root />) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ESNext", | ||
| "useDefineForClassFields": true, | ||
| "lib": ["ES2020", "DOM", "DOM.Iterable", "ESNext"], | ||
| "module": "ESNext", | ||
| "skipLibCheck": true, | ||
| /* Bundler mode */ | ||
| "moduleResolution": "Bundler", | ||
| "allowImportingTsExtensions": true, | ||
| "resolveJsonModule": true, | ||
| "isolatedModules": true, | ||
| "noEmit": true, | ||
| "jsx": "react-jsx", | ||
| /* Linting */ | ||
| "strict": true, | ||
| "noUnusedLocals": true, | ||
| "noUnusedParameters": true, | ||
| "noFallthroughCasesInSwitch": true | ||
| }, | ||
| "include": ["src", "eslint.config.js"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import{defineConfig}from'vite' | ||
| importreactfrom'@vitejs/plugin-react' | ||
| exportdefaultdefineConfig({ | ||
| plugins: [react()], | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Ignore Vite’s actual build output directory.
Lines 11-13 ignore
/build, butexamples/react/batching/vite.config.tsdoes not override Vite’s defaultoutDir, so this example will emitdist/instead. As written, generated artifacts can be committed accidentally.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents