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 id = 0 let list = [ 'apple', 'banana', 'pineapple', 'grapefruit', 'dragonfruit', 'grapes', ].map((d) => ({ id: id++, 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 queryClient = new QueryClient() function Root() { const [staleTime, setStaleTime] = React.useState(1000) const [gcTime, setGcTime] = React.useState(3000) const [localErrorRate, setErrorRate] = 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" value={staleTime} onChange={(e) => setStaleTime(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <div> Garbage collection Time:{' '} <input type="number" min="0" step="1000" value={gcTime} onChange={(e) => setGcTime(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <br /> <div> Error Rate:{' '} <input type="number" min="0" max="1" step=".05" value={localErrorRate} onChange={(e) => setErrorRate(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <div> Fetch Time Min:{' '} <input type="number" min="1" step="500" value={localFetchTimeMin} onChange={(e) => setLocalFetchTimeMin(parseFloat(e.target.value))} style={{ width: '60px' }} />{' '} </div> <div> Fetch Time Max:{' '} <input type="number" min="1" step="500" value={localFetchTimeMax} onChange={(e) => setLocalFetchTimeMax(parseFloat(e.target.value))} 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']) // const [views, setViews] = React.useState([""]); 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} onRemove={() => { setViews((old) => [...old, '']) }} /> <br /> </div> ))} <button onClick={() => { setViews((old) => [...old, '']) }} > Add Filter List </button> <hr /> {editingIndex !== null ? ( <> <EditTodo 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: fetchTodos, }) 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 ? data.map((todo) => ( <li key={todo.id}> {todo.name}{' '} <button onClick={() => setEditingIndex(todo.id)}> Edit </button> </li> )) : null} </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() // Don't attempt to query until editingIndex is truthy const { status, data, isFetching, error, failureCount, refetch } = useQuery({ queryKey: ['todo', { id: editingIndex }], queryFn: () => fetchTodoById({ id: editingIndex }), }) const [todo, setTodo] = React.useState(data || {}) React.useEffect(() => { if (editingIndex !== null && data) { setTodo(data) } else { setTodo({}) } }, [data, editingIndex]) const saveMutation = useMutation({ mutationFn: patchTodo, onSuccess: (data) => { // Update `todos` and the individual todo queries when this mutation succeeds queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.setQueryData(['todo', { id: editingIndex }], data) }, }) const onSave = () => { saveMutation.mutate(todo) } 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> ) : error ? ( <span> Error! <button onClick={() => refetch()}>Retry</button> </span> ) : ( <> <label> Name:{' '} <input value={todo.name} onChange={(e) => e.persist() || setTodo((old) => ({ ...old, name: e.target.value })) } disabled={disableEditSave} /> </label> <label> Notes:{' '} <input value={todo.notes} onChange={(e) => e.persist() || setTodo((old) => ({ ...old, notes: e.target.value })) } disabled={disableEditSave} /> </label> <div> <button onClick={onSave} 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({ signal, queryKey: [, { filter }] }): Promise<Todos> { console.info('fetchTodos', { filter }) if (signal) { 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)), ) } resolve(list.find((d) => d.id === id)) }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin), ) }) } function postTodo({ name, notes }: Omit<Todo, 'id'>) { 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: id++ } 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') as HTMLElement ReactDOM.createRoot(rootElement).render(<Root />)
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 id = 0 let list = [ 'apple', 'banana', 'pineapple', 'grapefruit', 'dragonfruit', 'grapes', ].map((d) => ({ id: id++, 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 queryClient = new QueryClient() function Root() { const [staleTime, setStaleTime] = React.useState(1000) const [gcTime, setGcTime] = React.useState(3000) const [localErrorRate, setErrorRate] = 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" value={staleTime} onChange={(e) => setStaleTime(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <div> Garbage collection Time:{' '} <input type="number" min="0" step="1000" value={gcTime} onChange={(e) => setGcTime(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <br /> <div> Error Rate:{' '} <input type="number" min="0" max="1" step=".05" value={localErrorRate} onChange={(e) => setErrorRate(parseFloat(e.target.value))} style={{ width: '100px' }} /> </div> <div> Fetch Time Min:{' '} <input type="number" min="1" step="500" value={localFetchTimeMin} onChange={(e) => setLocalFetchTimeMin(parseFloat(e.target.value))} style={{ width: '60px' }} />{' '} </div> <div> Fetch Time Max:{' '} <input type="number" min="1" step="500" value={localFetchTimeMax} onChange={(e) => setLocalFetchTimeMax(parseFloat(e.target.value))} 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']) // const [views, setViews] = React.useState([""]); 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} onRemove={() => { setViews((old) => [...old, '']) }} /> <br /> </div> ))} <button onClick={() => { setViews((old) => [...old, '']) }} > Add Filter List </button> <hr /> {editingIndex !== null ? ( <> <EditTodo 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: fetchTodos, }) 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 ? data.map((todo) => ( <li key={todo.id}> {todo.name}{' '} <button onClick={() => setEditingIndex(todo.id)}> Edit </button> </li> )) : null} </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() // Don't attempt to query until editingIndex is truthy const { status, data, isFetching, error, failureCount, refetch } = useQuery({ queryKey: ['todo', { id: editingIndex }], queryFn: () => fetchTodoById({ id: editingIndex }), }) const [todo, setTodo] = React.useState(data || {}) React.useEffect(() => { if (editingIndex !== null && data) { setTodo(data) } else { setTodo({}) } }, [data, editingIndex]) const saveMutation = useMutation({ mutationFn: patchTodo, onSuccess: (data) => { // Update `todos` and the individual todo queries when this mutation succeeds queryClient.invalidateQueries({ queryKey: ['todos'] }) queryClient.setQueryData(['todo', { id: editingIndex }], data) }, }) const onSave = () => { saveMutation.mutate(todo) } 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> ) : error ? ( <span> Error! <button onClick={() => refetch()}>Retry</button> </span> ) : ( <> <label> Name:{' '} <input value={todo.name} onChange={(e) => e.persist() || setTodo((old) => ({ ...old, name: e.target.value })) } disabled={disableEditSave} /> </label> <label> Notes:{' '} <input value={todo.notes} onChange={(e) => e.persist() || setTodo((old) => ({ ...old, notes: e.target.value })) } disabled={disableEditSave} /> </label> <div> <button onClick={onSave} 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({ signal, queryKey: [, { filter }] }): Promise<Todos> { console.info('fetchTodos', { filter }) if (signal) { 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)), ) } resolve(list.find((d) => d.id === id)) }, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin), ) }) } function postTodo({ name, notes }: Omit<Todo, 'id'>) { 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: id++ } 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') as HTMLElement ReactDOM.createRoot(rootElement).render(<Root />)