Docs
Code Rabbit
Cloudflare
AG Grid
Netlify
Neon
WorkOS
Clerk
Convex
Electric
Sentry
Prisma
Strapi
Unkey
UI.dev
Code Rabbit
Cloudflare
AG Grid
Netlify
Neon
WorkOS
Clerk
Convex
Electric
Sentry
Prisma
Strapi
Unkey
UI.dev

React Example: Playground

jsx
import React from 'react'
import ReactDOM from 'react-dom/client'

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useQueryClient,
  useMutation,
} 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' }))

let errorRate = 0.05
let queryTimeMin = 1000
let queryTimeMax = 2000

const queryClient = new QueryClient()

function Root() {
  const [staleTime, setStaleTime] = React.useState(1000)
  const [cacheTime, setCacheTime] = 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,
        cacheTime,
      },
    })
  }, [cacheTime, staleTime])

  return (
    <QueryClientProvider client={queryClient}>
      <p>
        The "staleTime" and "cacheTime" 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, 10))}
          style={{ width: '100px' }}
        />
      </div>
      <div>
        Cache Time:{' '}
        <input
          type="number"
          min="0"
          step="1000"
          value={cacheTime}
          onChange={(e) => setCacheTime(parseFloat(e.target.value, 10))}
          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, 10))}
          style={{ width: '100px' }}
        />
      </div>
      <div>
        Fetch Time Min:{' '}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMin}
          onChange={(e) => setLocalFetchTimeMin(parseFloat(e.target.value, 10))}
          style={{ width: '60px' }}
        />{' '}
      </div>
      <div>
        Fetch Time Max:{' '}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMax}
          onChange={(e) => setLocalFetchTimeMax(parseFloat(e.target.value, 10))}
          style={{ width: '60px' }}
        />
      </div>
      <br />
      <App />
      <br />
      <ReactQueryDevtools initialIsOpen />
    </QueryClientProvider>
  )
}

function App() {
  const queryClient = useQueryClient()
  const [editingIndex, setEditingIndex] = React.useState(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 }) {
  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 === 'loading' ? (
        <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>&nbsp;</span>
            )}
          </div>
        </>
      )}
    </div>
  )
}

function EditTodo({ editingIndex, setEditingIndex }) {
  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 }),
    enabled: editingIndex !== null,
  })

  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 === 'loading' || saveMutation.status === 'loading'

  return (
    <div>
      <div>
        {data ? (
          <>
            <button onClick={() => setEditingIndex(null)}>Back</button> Editing
            Todo "{data.name}" (#
            {editingIndex})
          </>
        ) : null}
      </div>
      {status === 'loading' ? (
        <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 === 'loading'
              ? 'Saving...'
              : saveMutation.status === 'error'
              ? saveMutation.error.message
              : 'Saved!'}
          </div>
          <div>
            {isFetching ? (
              <span>
                Background Refreshing... (Attempt: {failureCount + 1})
              </span>
            ) : (
              <span>&nbsp;</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 === 'loading'}
      />
      <button
        onClick={() => {
          addMutation.mutate({ name })
        }}
        disabled={addMutation.status === 'loading' || !name}
      >
        Add Todo
      </button>
      <div>
        {addMutation.status === 'loading'
          ? 'Saving...'
          : addMutation.status === 'error'
          ? addMutation.error.message
          : 'Saved!'}
      </div>
    </div>
  )
}

function fetchTodos({ signal, queryKey: [, { filter }] }) {
  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 }) {
  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 }) {
  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) {
  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')
ReactDOM.createRoot(rootElement).render(<Root />)
import React from 'react'
import ReactDOM from 'react-dom/client'

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
  useQueryClient,
  useMutation,
} 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' }))

let errorRate = 0.05
let queryTimeMin = 1000
let queryTimeMax = 2000

const queryClient = new QueryClient()

function Root() {
  const [staleTime, setStaleTime] = React.useState(1000)
  const [cacheTime, setCacheTime] = 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,
        cacheTime,
      },
    })
  }, [cacheTime, staleTime])

  return (
    <QueryClientProvider client={queryClient}>
      <p>
        The "staleTime" and "cacheTime" 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, 10))}
          style={{ width: '100px' }}
        />
      </div>
      <div>
        Cache Time:{' '}
        <input
          type="number"
          min="0"
          step="1000"
          value={cacheTime}
          onChange={(e) => setCacheTime(parseFloat(e.target.value, 10))}
          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, 10))}
          style={{ width: '100px' }}
        />
      </div>
      <div>
        Fetch Time Min:{' '}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMin}
          onChange={(e) => setLocalFetchTimeMin(parseFloat(e.target.value, 10))}
          style={{ width: '60px' }}
        />{' '}
      </div>
      <div>
        Fetch Time Max:{' '}
        <input
          type="number"
          min="1"
          step="500"
          value={localFetchTimeMax}
          onChange={(e) => setLocalFetchTimeMax(parseFloat(e.target.value, 10))}
          style={{ width: '60px' }}
        />
      </div>
      <br />
      <App />
      <br />
      <ReactQueryDevtools initialIsOpen />
    </QueryClientProvider>
  )
}

function App() {
  const queryClient = useQueryClient()
  const [editingIndex, setEditingIndex] = React.useState(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 }) {
  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 === 'loading' ? (
        <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>&nbsp;</span>
            )}
          </div>
        </>
      )}
    </div>
  )
}

function EditTodo({ editingIndex, setEditingIndex }) {
  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 }),
    enabled: editingIndex !== null,
  })

  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 === 'loading' || saveMutation.status === 'loading'

  return (
    <div>
      <div>
        {data ? (
          <>
            <button onClick={() => setEditingIndex(null)}>Back</button> Editing
            Todo "{data.name}" (#
            {editingIndex})
          </>
        ) : null}
      </div>
      {status === 'loading' ? (
        <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 === 'loading'
              ? 'Saving...'
              : saveMutation.status === 'error'
              ? saveMutation.error.message
              : 'Saved!'}
          </div>
          <div>
            {isFetching ? (
              <span>
                Background Refreshing... (Attempt: {failureCount + 1})
              </span>
            ) : (
              <span>&nbsp;</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 === 'loading'}
      />
      <button
        onClick={() => {
          addMutation.mutate({ name })
        }}
        disabled={addMutation.status === 'loading' || !name}
      >
        Add Todo
      </button>
      <div>
        {addMutation.status === 'loading'
          ? 'Saving...'
          : addMutation.status === 'error'
          ? addMutation.error.message
          : 'Saved!'}
      </div>
    </div>
  )
}

function fetchTodos({ signal, queryKey: [, { filter }] }) {
  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 }) {
  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 }) {
  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) {
  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')
ReactDOM.createRoot(rootElement).render(<Root />)