Server-state manager
Stop syncing server data by hand.
Query gives async data a cache, a lifecycle, and a set of declarative APIs for fetching, sharing, refetching, mutating, and observing server state across TypeScript applications.
00.0 MillionTotal Downloads000,000,000Weekly Downloads0GitHub StarsThe server-state standard for modern frontend apps.
Server-state cache
freshness, retries, gc, dedupe
Mutation workflow
optimistic UI, rollback, invalidate
Framework adapters
React, Vue, Solid, Svelte, Angular, Lit
['issues', 'router-cache']
Router dashboard
['issues', 'project-detail']
Project detail
['issues', 'offline-queue']
Offline mutation queue
useQuery()
Components declare the data they need. The cache coordinates fetches, subscribers, freshness, and background updates.
Why Query
Server data is remote, shared, cached, refetched, invalidated, and sometimes stale on purpose. Query handles that lifecycle directly instead of making you recreate it with reducers, effects, and synchronized stores.
Caching, request dedupe, retries, background refetching, window-focus updates, and garbage collection are already wired for the shape of real apps.
Keys describe the resource, inputs, filters, and scope so reads, writes, invalidation, prefetching, and devtools all speak the same language.
Handle pending UI, optimistic writes, invalidation, rollback, and follow-up refetches without inventing an ad hoc client-state machine.
See query keys, observers, freshness, retries, errors, mutations, and cache contents while the app is actually running.
A query function resolves data or throws. Query owns retry, cancellation, and deduping.
Every observer reads the same cache entry instead of refetching from every component.
Stale data can stay on screen while a background refetch quietly refreshes it.
Unused data sticks around long enough to feel instant, then garbage collection cleans up.
queryKey: ['projects', filters]
queryFn: fetchProjects
staleTime: 30_000
gcTime: 300_000
Cache lifecycle
Query lets stale data remain valuable. Screens can render instantly from cache, refetch in the background, keep previous results during pagination, and recover when the user comes back online.
Mutations
Query keeps mutation work explicit: optimistic updates, pending states, error recovery, invalidation, and background reconciliation are first-class instead of scattered through components.
setQueryData(['todos'], next)
await saveTodo(todo)
invalidateQueries(['todos'])
onError: restoreSnapshot
Framework adapters
The core cache model travels across frameworks. Teams can keep the same query keys, invalidation strategy, mutation semantics, and mental model whether the UI is React, Vue, Solid, Svelte, Angular, Preact, or Lit.
import { useQuery } from '@tanstack/react-query'
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
if (isPending) return <span>Loading...</span>
if (error) return <span>Oops!</span>
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}
export default Todosimport { useQuery } from '@tanstack/react-query'
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
if (isPending) return <span>Loading...</span>
if (error) return <span>Oops!</span>
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}
export default Todosimport { useQuery } from '@tanstack/preact-query'
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
if (isPending) return <span>Loading...</span>
if (error) return <span>Oops!</span>
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}
export default Todosimport { useQuery } from '@tanstack/preact-query'
function Todos() {
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
if (isPending) return <span>Loading...</span>
if (error) return <span>Oops!</span>
return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}
export default Todosimport { createQuery } from '@tanstack/solid-query'
function Todos() {
const todos = createQuery(() => ({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
}))
return <ul>{todos.data?.map((t) => <li>{t.title}</li>)}</ul>
}
export default Todosimport { createQuery } from '@tanstack/solid-query'
function Todos() {
const todos = createQuery(() => ({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
}))
return <ul>{todos.data?.map((t) => <li>{t.title}</li>)}</ul>
}
export default Todos<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
</script>
<template>
<ul v-if="data">
<li v-for="t in data" :key="t.id">{{ t.title }}</li>
</ul>
<span v-else-if="isPending">Loading...</span>
<span v-else>Oops!</span>
</template><script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
</script>
<template>
<ul v-if="data">
<li v-for="t in data" :key="t.id">{{ t.title }}</li>
</ul>
<span v-else-if="isPending">Loading...</span>
<span v-else>Oops!</span>
</template><script lang="ts">
import { createQuery } from '@tanstack/svelte-query'
const todos = createQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
</script>
{#if $todos.isPending}
Loading...
{:else if $todos.error}
Oops!
{:else}
<ul>
{#each $todos.data as t}
<li>{t.title}</li>
{/each}
</ul>
{/if}<script lang="ts">
import { createQuery } from '@tanstack/svelte-query'
const todos = createQuery({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
</script>
{#if $todos.isPending}
Loading...
{:else if $todos.error}
Oops!
{:else}
<ul>
{#each $todos.data as t}
<li>{t.title}</li>
{/each}
</ul>
{/if}import { Component } from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
@Component({
selector: 'todos',
standalone: true,
template: `
<ng-container *ngIf="todos.isPending()">
Loading...
</ng-container>
<ul *ngIf="todos.data() as data">
<li *ngFor="let t of data">
{{ t.title }}
</li>
</ul>
`,
})
export class TodosComponent {
todos = injectQuery(() => ({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
}))
}import { Component } from '@angular/core'
import { injectQuery } from '@tanstack/angular-query-experimental'
@Component({
selector: 'todos',
standalone: true,
template: `
<ng-container *ngIf="todos.isPending()">
Loading...
</ng-container>
<ul *ngIf="todos.data() as data">
<li *ngFor="let t of data">
{{ t.title }}
</li>
</ul>
`,
})
export class TodosComponent {
todos = injectQuery(() => ({
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
}))
}import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { createQueryController } from '@tanstack/lit-query'
@customElement('todos-list')
export class TodosList extends LitElement {
private todos = createQueryController(this, {
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
render() {
const { data, isPending, error } = this.todos.current
if (isPending) return html`<span>Loading...</span>`
if (error) return html`<span>Oops!</span>`
return html`<ul>${data.map(t => html`<li>${t.title}</li>`)}</ul>`
}
}import { LitElement, html } from 'lit'
import { customElement } from 'lit/decorators.js'
import { createQueryController } from '@tanstack/lit-query'
@customElement('todos-list')
export class TodosList extends LitElement {
private todos = createQueryController(this, {
queryKey: ['todos'],
queryFn: () => fetch('/api/todos').then(r => r.json()),
})
render() {
const { data, isPending, error } = this.todos.current
if (isPending) return html`<span>Loading...</span>`
if (error) return html`<span>Oops!</span>`
return html`<ul>${data.map(t => html`<li>${t.title}</li>`)}</ul>`
}
}Field notes
The original copy was right: Query saves code by deleting whole categories of hand-written fetching, loading, retry, cache, and mutation logic.
See what teams are saying
"Honestly, if React Query had been around before Redux, I don't think Redux would have been nearly as popular as it was."
"If I could go back in time and mass myself... I would hand myself a flash drive with a copy of react-query on it."
"React Query won. There's no denying that."
"TanStack Query has been a game-changer for us. We love using it for react-admin."
"The more I use React + Vite + TanStack Router + TypeScript + TanStack Query, the more I love it."
"Combined with React Query, this stack has been a game-changer for my productivity."
"Honestly, if React Query had been around before Redux, I don't think Redux would have been nearly as popular as it was."
"If I could go back in time and mass myself... I would hand myself a flash drive with a copy of react-query on it."
"React Query won. There's no denying that."
"TanStack Query has been a game-changer for us. We love using it for react-admin."
"The more I use React + Vite + TanStack Router + TypeScript + TanStack Query, the more I love it."
"Combined with React Query, this stack has been a game-changer for my productivity."
Open source ecosystem
Query is built in public and taught in public. The maintainers, partner integrations, Query.gg, and GitHub sponsors all stay close to the product story.