Point0by 1gr14
  • Меню
    • GitHub
    • Блог
  • Introduction
    • Overview
    • Getting Started
    • Full Overview
    • Benchmarks
    • Points
  • Points
    • Page
    • Layout
    • Component
    • Provider
    • Mountable
    • Query
    • Infinite Query
    • Mutation
    • Action
    • Root
    • Base
    • Plugin
  • Methods
    • Validation
    • Loader
    • Context
    • Middleware
    • Loading & error
    • .with
    • Mapper
    • Transformer
    • Stage Methods
  • Core
    • Navigation
    • SSR
    • RSC
    • Request
    • Response
    • Error handling
    • Env
    • Head
    • MDX
    • Assets
    • File upload
    • OpenAPI
    • Query client
    • Events
    • Infer
  • Engine
    • Engine Config
    • Engine Runtime
    • CLI
    • Dev
    • Build
    • Compiler
    • Generator
    • Project MCP
    • Docs MCP
    • Importer
    • Public dir
    • Testing
    • Deploy
    • Bun or Vite
  • Extra
    • SsrStore
    • CookieStore
    • Basic Auth
    • CORS
    • Cache-Control
    • Compress
  • Examples
    • Basic
    • Vite
    • Better Auth
    • Capacitor
    • Expo
  • 1gr14
    • Главная
    • Start0
    • Поддержать
    • Обучение
    • Группа
    • Блог
    • Автор
  • Сообщество
    • Discord
    • Telegram
  • Опенсорс
    • Point0
    • Route0
    • Error0
    • Flat
  • Аккаунт
    • Войти
    • Регистрация
Point0by 1gr14
Создаю опенсорс во славу Господа Иисуса Христа ☦️
С любовью ко всем разработчикам на свете ❤️
Условия использованияПолитика конфиденциальностиСергей Дмитриев 2026 😎

Query client

  • Раздел: Core

On this page

  1. createQueryClient returns a proxy, not a client
  2. The QueryClientProvider is yours to wire
  3. Reaching the client
  4. Matching cache entries: getQueryPredicate
  5. Targeting a specific client: { queryClient }
  6. One client per request, one per tab
  7. SSR: dehydrate on the server, hydrate on the client
  8. Security: the dehydrated cache is public
  9. Reference
  10. createQueryClient(getConfig?)
  11. getQueryClient()
  12. getQueryPredicate(options) / getMutationPredicate(options)
  13. The { queryClient } option
  14. The global item

Point0 runs every query and mutation through TanStack Query's QueryClient. You create it once and hand it to React Query's provider — Point0 holds it, gives each SSR request its own instance, and ships its cache to the browser.

// src/lib/query-client.ts
import { createQueryClient } from '@point0/core'

// Safe on both server and client — each SSR request gets its own instance.
export const queryClient = createQueryClient()
// src/app.client.tsx
import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/query-client'

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        <RouterRoutes />
      </Router>
    </QueryClientProvider>
  )
}

That's the whole setup.

createQueryClient returns a proxy, not a client

The value you export is not a real QueryClient — it's a proxy that always forwards to whichever real client is current:

export const queryClient = createQueryClient()
// queryClient is typed as QueryClient, but every method call is delegated, at
// call time, to the active client for the current context.

On the server, each request runs in its own context with its own QueryClient; the same queryClient.invalidateQueries(...) line resolves to the right per-request instance, so one user's data never leaks into another's render. In the browser there's a single client for the tab, so the proxy always lands on it.

The optional argument is a function returning a TanStack QueryClientConfig — Point0 constructs each real client itself and merges your config over its own defaults, so framework defaults survive app customization instead of being silently overwritten:

export const queryClient = createQueryClient(() => ({
  defaultOptions: { queries: { staleTime: 60_000 } },
}))

Top-level keys and defaultOptions.queries keys you set win; defaults you don't touch stay. Point0 ships one default of its own — the RSC-safe structuralSharing (element-containing query data is handed back fresh instead of deep-merged; see RSC) — override it by setting structuralSharing yourself. Per-query defaults are also settable higher in the chain with .queryOptions.

The QueryClientProvider is yours to wire

Point0 does not render QueryClientProvider for you — mount() only mounts the points and hydrates the store. Wire the provider yourself in app.client.tsx, passing the proxy as client:

import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from '@/lib/query-client'
;<QueryClientProvider client={queryClient}>
  <ReactQueryDevtools initialIsOpen={true} />
  {/* your app */}
</QueryClientProvider>

React Query calls methods on the client you give it; because the proxy forwards each call to the live instance, the provider stays correct across SSR requests without a fresh provider per request.

Reaching the client

Most code never touches the client directly — useQuery, fetchQuery, setQueryData, and the rest live on each query and mutation point. When you do need the raw client, you have two routes.

The queryClient module export. Import it anywhere — inside a loader, a component, an event handler — and call TanStack methods on it. Clear the whole cache on a sign-out or identity change:

import { queryClient } from '@/lib/query-client'

await authClient.admin.impersonateUser({ userId: id })
queryClient.clear() // drop every cached query
void navigate('profile', undefined, { replace: true })

Pair a raw invalidateQueries with a point-scoped predicate to invalidate one point's queries by hand after a mutation:

import { getQueryPredicate } from '@point0/core'

const { idea } = await ideaCreateMutation.fetch({ title, content })
void queryClient.invalidateQueries({
  predicate: getQueryPredicate({ id: ideaListQuery.id }), // every ideaListQuery cache entry
})
ideaViewQuery.setQueryData({ sn: idea.sn }, { idea }) // seed the view cache

For the common case — every cache entry of one point regardless of input — the point's own helper is shorter: ideaListQuery.invalidateQuery(true). The same true (or a (input) => boolean predicate) works on refetchQuery, removeQuery, resetQuery, cancelQuery and their infinite siblings. Reach for a raw invalidateQueries + getQueryPredicate when you need to match across points or by tag.

getQueryClient() returns the real current client (not the proxy) for the active context:

import { getQueryClient } from '@point0/core'

const client = getQueryClient() // the live QueryClient for this request / tab

It takes no arguments and always resolves the ambient client.

Don't call queryClient at module top level on the server. The proxy resolves against the active request context, which only exists inside a server run. Use it inside loaders, components, hooks, and handlers — never at import time.

Matching cache entries: getQueryPredicate

Point0 stores every query under a structured key, so you rarely match by raw TanStack key. getQueryPredicate(options) builds a (query) => boolean predicate from a point's identity instead — drop it into any TanStack call that takes one (invalidateQueries, removeQueries, getQueryCache().findAll, …):

import { getQueryPredicate } from '@point0/core'

// Every cache entry belonging to one point
queryClient.getQueryCache().findAll({
  predicate: getQueryPredicate({ id: ideaListQuery.id }),
})

It only ever matches Point0's own queries (entries keyed under 'point0'); plain TanStack queries you registered yourself are skipped. Every option you pass is an AND filter — omit an option and it's ignored. The fields mirror a point's identity:

OptionMatches
idthe point id <scope>:<type>:<name> — pass somePoint.id, the cleanest way to target one point
scopethe point's scope (e.g. 'root')
typethe point type ('query', 'page', …)
namethe point name
tagsthe point's tags — see below
mode'server' or 'client' query mode
finiteness'finite' or 'infinite' (an infinite query)
outputthe fetch output type the cache entry holds

tags accepts three shapes: a single string matches entries carrying that tag; an array matches only entries that carry all of them; a function (tags: string[]) => boolean lets you decide:

getQueryPredicate({ tags: 'my-tag' }) // has 'my-tag'
getQueryPredicate({ tags: ['a', 'b'] }) // has BOTH 'a' and 'b'
getQueryPredicate({ tags: (tags) => tags.includes('a') })

// Combine identity filters — AND across options
getQueryPredicate({
  scope: 'root',
  type: 'query',
  name: 'ideaList',
  mode: 'client',
})

For matching mutations rather than queries, there's a parallel getMutationPredicate with the same id / scope / type / name / tags options, returning a (mutation) => boolean predicate for the mutation cache.

Targeting a specific client: { queryClient }

Every imperative cache and fetch helper on a query or mutation takes a trailing options object with an optional queryClient:

await ideaQuery.fetchQuery({ id }, undefined, { queryClient: someOtherClient })
ideaQuery.invalidateQuery({ id }, undefined, { queryClient: someOtherClient })
const m = ideaCreateMutation.getMutationCache(
  { id: 7 },
  { queryClient: someOtherClient },
)

When you omit it — the normal case — the helper uses the ambient global client, which the proxy already resolves to the correct per-request (server) or singleton (client) instance:

await ideaQuery.fetchQuery({ id }) // uses the ambient client — what you want almost always

So { queryClient } is an escape hatch: pass it only when you already hold a second client and need to act on that one (a manual multi-client setup, or hand-rolled SSR control). Build the second QueryClient yourself and thread it through.

One client per request, one per tab

Server: one QueryClient per request. Each incoming request gets a fresh client from your init factory, so caches never mix across users. A nested or recursive server fetch reuses the parent request's client, so the inner run shares the same cache instead of starting cold.

Client: one shared QueryClient per tab. The proxy resolves to the same instance everywhere — which is what QueryClientProvider client={queryClient} needs.

Multiple apps share one proxy. Even with several mounted apps (different scopes, e.g. an admin app and a public app on one server), you keep a single createQueryClient() module. The proxy resolves the right underlying client per context; you don't create a distinct QueryClient per scope.

SSR: dehydrate on the server, hydrate on the client

The server renders with its request-scoped client, serializes the client's cache into the HTML, and the browser rehydrates it — so the first paint already has the data and the client doesn't refetch what the server already fetched.

You write none of this. The client is registered with Point0's store as a transferable item, so it rides along automatically: the server injects window.__POINT0_DEHYDRATED_SUPER_STORE__ into the page, and mount() reads it back and hydrates on first access. See SSR and the SsrStore for the transport mechanics.

Two Point0-specific things happen during that roundtrip:

Errors are projected for the wire. Errors sitting in the cache (a failed query) are serialized before they cross to the browser — the public projection in production, the private one in development, where the developer is the audience. This means a production client never receives a server stack trace or private error fields. See Error handling.

Hydrated data is treated as fresh. On hydration Point0 rewrites the cache timestamps to "now", so data that arrived with the HTML isn't immediately considered stale and refetched on mount.

When a page is prefetched via the pageDehydratedState policy, the dehydrated cache is narrowed to just that page's snapshot, and a re-prefetch from a still-fresh snapshot reads the current store rather than resurrecting a query the user removed in the meantime (e.g. getMe after sign-out). Those prefetch policies live under navigation (and SSR).

Security: the dehydrated cache is public

When SSR is enabled Point0 server-renders the first page load; only after that first render does navigation between pages go client-side (SPA-style). Either way, whatever the server puts in the cache and dehydrates is serialized into the page and visible in the browser — don't cache secret data expecting it to stay on the server. Gate access in a .with wrapper (and resolve server-side identity in .ctx for loaders), and let the error projection above keep private error detail off the wire.

Reference

createQueryClient(getConfig?)

createQueryClient(getConfig?: () => QueryClientConfig) // => QueryClient (a proxy)
  • getConfig — optional function returning a TanStack QueryClientConfig. Point0 builds each real client as new QueryClient(merge(point0Defaults, getConfig())) — your keys win, the defaults you don't touch stay (today that's the RSC-safe structuralSharing). Returning a QueryClient instance throws — the client is Point0's to construct. You cannot replace a built client instance later (the item is read-only); you only replace the config, and only by calling createQueryClient again.
  • Returns the proxy. Use it as queryClient everywhere, including as the QueryClientProvider client prop.

getQueryClient()

getQueryClient() // => QueryClient (the real current client)

Returns the live client for the active context. No arguments. Only valid inside a server run or in the browser — not at module top level on the server.

getQueryPredicate(options) / getMutationPredicate(options)

getQueryPredicate(options) // => (query: Query) => boolean
getMutationPredicate(options) // => (mutation: Mutation) => boolean

Build a predicate that matches Point0's own cache entries by point identity, for any TanStack call that takes a predicate. Options (all optional, AND-combined): id (<scope>:<type>:<name>), scope, type, name, tags (string · all-of array · (tags) => boolean). getQueryPredicate additionally takes mode ('server'/'client'), finiteness ('finite'/'infinite'), and output. Non-Point0 entries never match. See Matching cache entries.

The { queryClient } option

Accepted on the imperative cache/fetch helpers of every query and mutation — fetchQuery, prefetchQuery, ensureQueryData, getQueryData, setQueryData, refetchQuery, invalidateQuery, cancelQuery, removeQuery, resetQuery, getQueryState, getQueryCache, getQueriesCache, their infinite-query mirrors, and the mutation cache accessors (getMutationCache, getMutationsCache, fetchMutation). Always optional; defaults to the ambient client. The position varies by method — it's a member of the trailing options object, not a positional argument (see the method tables on Query and Mutation).

The global item

The client lives in Point0's store under the name __POINT0_QUERY_CLIENT__. You normally reach it through the queryClient proxy or getQueryClient(); the raw item is an advanced surface. It is read-only: you can swap the factory but not .set a client instance, which is what keeps per-request isolation intact.

НазадOpenAPIДалееEvents

Enjoying Point0?

Give it a star on GitHub, ask questions, and follow what's new

Star Point0

A star on GitHub helps more developers find Point0
Star on GitHub

Start0

Point0 integrated into the Start0 SaaS boilerplate — the best way to use it
Explore Start0

YouTube

Tutorials, walkthroughs, and updates
YouTube

Discord

Questions and chat in English
Join Discord

Telegram

News channel and chat in Russian
ChannelChat

Twitter

Posts and updates
Twitter