import React from 'react'
import ReactDOM from 'react-dom/client'
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import './styles.css'
let nextId = 0
let list = [
'apple',
'banana',
'pineapple',
'grapefruit',
'dragonfruit',
'grapes',
].map((d) => ({ id: nextId++, name: d, notes: 'These are some notes' }))
type Todos = typeof list
type Todo = Todos[0]
let errorRate = 0.05
let queryTimeMin = 1000
let queryTimeMax = 2000
const initialStaleTime = 1000
const initialGcTime = 3000
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: initialStaleTime,
gcTime: initialGcTime,
},
},
})
function onNumberChange(setValue: (value: number) => void) {
return (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.valueAsNumber
if (!Number.isNaN(value)) {
setValue(value)
}
}
}
function Root() {
const [staleTime, setStaleTime] = React.useState(initialStaleTime)
const [gcTime, setGcTime] = React.useState(initialGcTime)
const [localErrorRate, setLocalErrorRate] = React.useState(errorRate)
const [localFetchTimeMin, setLocalFetchTimeMin] = React.useState(queryTimeMin)
const [localFetchTimeMax, setLocalFetchTimeMax] = React.useState(queryTimeMax)
React.useEffect(() => {
errorRate = localErrorRate
queryTimeMin = localFetchTimeMin
queryTimeMax = localFetchTimeMax
}, [localErrorRate, localFetchTimeMax, localFetchTimeMin])
React.useEffect(() => {
queryClient.setDefaultOptions({
queries: {
staleTime,
gcTime,
},
})
}, [gcTime, staleTime])
return (
<QueryClientProvider client={queryClient}>
<p>
The "staleTime" and "gcTime" durations have been altered in this example
to show how query stale-ness and query caching work on a granular level
</p>
<div>
Stale Time:{' '}
<input
type="number"
min="0"
step="1000"
defaultValue={staleTime}
onChange={onNumberChange(setStaleTime)}
style={{ width: '100px' }}
/>
</div>
<div>
Garbage collection Time:{' '}
<input
type="number"
min="0"
step="1000"
defaultValue={gcTime}
onChange={onNumberChange(setGcTime)}
style={{ width: '100px' }}
/>
</div>
<br />
<div>
Error Rate:{' '}
<input
type="number"
min="0"
max="1"
step=".05"
defaultValue={localErrorRate}
onChange={onNumberChange(setLocalErrorRate)}
style={{ width: '100px' }}
/>
</div>
<div>
Fetch Time Min:{' '}
<input
type="number"
min="1"
step="500"
defaultValue={localFetchTimeMin}
onChange={onNumberChange(setLocalFetchTimeMin)}
style={{ width: '60px' }}
/>{' '}
</div>
<div>
Fetch Time Max:{' '}
<input
type="number"
min="1"
step="500"
defaultValue={localFetchTimeMax}
onChange={onNumberChange(setLocalFetchTimeMax)}
style={{ width: '60px' }}
/>
</div>
<br />
<App />
<br />
<ReactQueryDevtools initialIsOpen />
</QueryClientProvider>
)
}
function App() {
const queryClient = useQueryClient()
const [editingIndex, setEditingIndex] = React.useState<number | null>(null)
const [views, setViews] = React.useState(['', 'fruit', 'grape'])
return (
<div className="App">
<div>
<button onClick={() => queryClient.invalidateQueries()}>
Force Refetch All
</button>
</div>
<br />
<hr />
{views.map((view, index) => (
<div key={index}>
<Todos initialFilter={view} setEditingIndex={setEditingIndex} />
<br />
</div>
))}
<button
onClick={() => {
setViews((old) => [...old, ''])
}}
>
Add Filter List
</button>
<hr />
{editingIndex !== null ? (
<>
<EditTodo
key={editingIndex}
editingIndex={editingIndex}
setEditingIndex={setEditingIndex}
/>
<hr />
</>
) : null}
<AddTodo />
</div>
)
}
function Todos({
initialFilter = '',
setEditingIndex,
}: {
initialFilter: string
setEditingIndex: React.Dispatch<React.SetStateAction<number | null>>
}) {
const [filter, setFilter] = React.useState(initialFilter)
const { status, data, isFetching, error, failureCount, refetch } = useQuery({
queryKey: ['todos', { filter }],
queryFn: ({ signal }) => fetchTodos({ filter, signal }),
})
return (
<div>
<div>
<label>
Filter:{' '}
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
</label>
</div>
{status === 'pending' ? (
<span>Loading... (Attempt: {failureCount + 1})</span>
) : status === 'error' ? (
<span>
Error: {error.message}
<br />
<button onClick={() => refetch()}>Retry</button>
</span>
) : (
<>
<ul>
{data.map((todo) => (
<li key={todo.id}>
{todo.name}{' '}
<button onClick={() => setEditingIndex(todo.id)}>Edit</button>
</li>
))}
</ul>
<div>
{isFetching ? (
<span>
Background Refreshing... (Attempt: {failureCount + 1})
</span>
) : (
<span> </span>
)}
</div>
</>
)}
</div>
)
}
function EditTodo({
editingIndex,
setEditingIndex,
}: {
editingIndex: number
setEditingIndex: React.Dispatch<React.SetStateAction<number | null>>
}) {
const queryClient = useQueryClient()
const { status, data, isFetching, failureCount, refetch } = useQuery({
queryKey: ['todo', { id: editingIndex }],
queryFn: () => fetchTodoById({ id: editingIndex }),
})
const [draft, setDraft] = React.useState<Partial<Omit<Todo, 'id'>>>({})
const saveMutation = useMutation({
mutationFn: patchTodo,
onSuccess: (todo) => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.setQueryData(['todo', { id: editingIndex }], todo)
setDraft({})
},
})
const disableEditSave =
status === 'pending' || saveMutation.status === 'pending'
return (
<div>
<div>
{data ? (
<>
<button onClick={() => setEditingIndex(null)}>Back</button> Editing
Todo "{data.name}" (#
{editingIndex})
</>
) : null}
</div>
{status === 'pending' ? (
<span>Loading... (Attempt: {failureCount + 1})</span>
) : status === 'error' ? (
<span>
Error! <button onClick={() => refetch()}>Retry</button>
</span>
) : (
<>
<label>
Name:{' '}
<input
value={draft.name ?? data.name}
onChange={(e) =>
setDraft((old) => ({ ...old, name: e.target.value }))
}
disabled={disableEditSave}
/>
</label>
<label>
Notes:{' '}
<input
value={draft.notes ?? data.notes}
onChange={(e) =>
setDraft((old) => ({ ...old, notes: e.target.value }))
}
disabled={disableEditSave}
/>
</label>
<div>
<button
onClick={() => saveMutation.mutate({ ...data, ...draft })}
disabled={disableEditSave}
>
Save
</button>
</div>
<div>
{saveMutation.status === 'pending'
? 'Saving...'
: saveMutation.status === 'error'
? saveMutation.error.message
: 'Saved!'}
</div>
<div>
{isFetching ? (
<span>
Background Refreshing... (Attempt: {failureCount + 1})
</span>
) : (
<span> </span>
)}
</div>
</>
)}
</div>
)
}
function AddTodo() {
const queryClient = useQueryClient()
const [name, setName] = React.useState('')
const addMutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
disabled={addMutation.status === 'pending'}
/>
<button
onClick={() => {
addMutation.mutate({ name, notes: 'These are some notes' })
}}
disabled={addMutation.status === 'pending' || !name}
>
Add Todo
</button>
<div>
{addMutation.status === 'pending'
? 'Saving...'
: addMutation.status === 'error'
? addMutation.error.message
: 'Saved!'}
</div>
</div>
)
}
function fetchTodos({
filter,
signal,
}: {
filter: string
signal: AbortSignal
}): Promise<Todos> {
console.info('fetchTodos', { filter })
signal.addEventListener('abort', () => {
console.info('cancelled', filter)
})
return new Promise((resolve, reject) => {
setTimeout(
() => {
if (Math.random() < errorRate) {
return reject(
new Error(JSON.stringify({ fetchTodos: { filter } }, null, 2)),
)
}
resolve(list.filter((d) => d.name.includes(filter)))
},
queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin),
)
})
}
function fetchTodoById({ id }: { id: number }): Promise<Todo> {
console.info('fetchTodoById', { id })
return new Promise((resolve, reject) => {
setTimeout(
() => {
if (Math.random() < errorRate) {
return reject(
new Error(JSON.stringify({ fetchTodoById: { id } }, null, 2)),
)
}
const todo = list.find((d) => d.id === id)
if (!todo) {
return reject(new Error(`Todo with id ${id} not found`))
}
resolve(todo)
},
queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin),
)
})
}
function postTodo({ name, notes }: Omit<Todo, 'id'>): Promise<Todo> {
console.info('postTodo', { name, notes })
return new Promise((resolve, reject) => {
setTimeout(
() => {
if (Math.random() < errorRate) {
return reject(
new Error(JSON.stringify({ postTodo: { name, notes } }, null, 2)),
)
}
const todo = { name, notes, id: nextId++ }
list = [...list, todo]
resolve(todo)
},
queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin),
)
})
}
function patchTodo(todo: Todo): Promise<Todo> {
console.info('patchTodo', todo)
return new Promise((resolve, reject) => {
setTimeout(
() => {
if (Math.random() < errorRate) {
return reject(new Error(JSON.stringify({ patchTodo: todo }, null, 2)))
}
list = list.map((d) => {
if (d.id === todo.id) {
return todo
}
return d
})
resolve(todo)
},
queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin),
)
})
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Missing #root element')
ReactDOM.createRoot(rootElement).render(<Root />)