Loading and Caching Server Data
The query configuration controls how remote reads are cached, refreshed and
shared. Charts and Pivot, Scheduler and Kanban and Gantt also use this engine; their integration guides describe ownership and refresh lifetimes. It is available on pagination, infinity scroll, remote trees and server-side grouping.
The loading engine is built into Pro: keep your existing loadData or loadRows
callback and add only the options you need. No separate Query package setup is
required for ordinary loading or polling.
| Grid configuration | Existing loader | Public configuration type |
|---|---|---|
grid.pagination | loadData(skip, take, ...) | PaginationConfig |
grid.infinityScroll | loadData(skip, limit, ...) | InfinityScrollConfig<Row> |
grid.tree.remote | loadData(skip, limit, ..., context) | TreeRemoteConfig<Row> |
grid.serverSideGrouping | loadRows(request, signal?), optional loadExpandedGroups(request, signal?) | ServerSideGroupingConfig<Row> |
Pagination without loadData remains local pagination. Query options apply to remote reads.
Quick start: poll existing data
Section titled “Quick start: poll existing data”This infinity-scroll configuration refreshes retained blocks every five seconds. See pages, blocks and routes for what is retained, polling for the interval/background options and focus refetch for the visibility trigger:
import { InfinityScrollPlugin, type InfinityScrollConfig,} from '@revolist/revogrid-pro/infinity-scroll';
type Order = { id: number; name: string };const grid = document.querySelector<HTMLRevoGridElement>('revo-grid')!;grid.plugins = [InfinityScrollPlugin];
const config: Partial<InfinityScrollConfig<Order>> = { chunkSize: 50, loadData: async (skip, limit) => { const response = await fetch(`/api/orders?skip=${skip}&limit=${limit}`); if (!response.ok) { throw Object.assign(new Error('Could not load orders'), { status: response.status }); } return response.json() as Promise<{ data: Order[]; total: number }>; }, query: { refetchInterval: 5_000, refetchIntervalInBackground: false, refetchOnWindowFocus: true, },};grid.infinityScroll = config;Use the same query fields inside grid.pagination or grid.serverSideGrouping.
For an existing pagination configuration, preserve its loader and other query
settings, then pass the result to your pagination property/framework binding:
// existingConfig is your current pagination configuration.const pagination: Partial<PaginationConfig> = { ...existingConfig, query: { ...existingConfig.query, refetchInterval: 5_000 },};Polling uses TanStack’s native observer timers and is independent of staleTime.
It refreshes retained pages, blocks and routes using their current request inputs;
it does not download the entire dataset. Set refetchInterval: false to stop
polling, or enabled: false to disable automatic reads. Apply changes by assigning
a new configuration object. Unchanged object reassignments are ignored by the
plugin’s framework integration.
Bypass Query for an application-owned cache
Section titled “Bypass Query for an application-owned cache”Set query: false when your existing loader uses its own query library/cache:
import { InfinityScrollPlugin, type InfinityScrollConfig } from '@revolist/revogrid-pro/infinity-scroll';
const grid = document.querySelector<HTMLRevoGridElement>('revo-grid')!;grid.plugins = [InfinityScrollPlugin];const config: Partial<InfinityScrollConfig> = { chunkSize: 50, query: false, async loadData(skip, limit) { const response = await fetch(`/api/orders?skip=${skip}&limit=${limit}`); if (!response.ok) throw new Error('Unable to load orders'); return response.json(); // { data: [...], total: number } },};grid.infinityScroll = config;The same query: false setting works for pagination and server-side grouping,
including loadExpandedGroups. Existing loader arguments and result shapes stay
unchanged. The plugin still requests pages/blocks/routes and projects rows into
the grid. It retains displayed blocks according to its normal viewport/grouping
rules; bypass does not automatically reload already displayed blocks on every
scroll or group reopen.
Bypass creates no QueryClient, query observer or query result cache for this
loader. Each new read requested by the plugin calls your callback. There are no
native polling/focus/reconnect triggers, automatic retries, cross-grid request
sharing, source acceptance fences or reconcile/onStatus query callbacks.
Your application owns those policies and any external cache invalidation.
Same-view pending reads are coalesced; concurrency limits, debounce, AbortSignal
forwarding and protection against obsolete results after reset/disposal remain.
Forward the optional signal to your transport when it supports cancellation.
| Configuration | Behavior |
|---|---|
Omit query, or use a query options object | Built-in Query engine |
query: { enabled: false } | Query remains configured; automatic reads pause |
query: false | Execute plugin-requested reads without the Query engine |
To switch modes, assign a new loader configuration object. The old controller is disposed before the replacement starts. Bypass is a runtime choice; the Pro bundle still includes its TanStack dependency.
Configuration types
Section titled “Configuration types”Import configuration types through the public loader entrypoints. Extract the query type from each configuration so result reconciliation stays correctly typed:
The property accepts false as well as an options object. Use Exclude<..., false | undefined>
for an options-only type, or narrow with if (config.query) before reading its fields.
import type { PaginationConfig } from '@revolist/revogrid-pro/pagination';import type { InfinityScrollConfig } from '@revolist/revogrid-pro/infinity-scroll';import type { ServerSideGroupingConfig } from '@revolist/revogrid-pro/server-side-grouping';
type Order = { id: number; name: string };type PaginationQuery = Exclude<PaginationConfig['query'], false | undefined>;type InfinityQuery = Exclude<InfinityScrollConfig<Order>['query'], false | undefined>;type GroupingQuery = Exclude<ServerSideGroupingConfig<Order>['query'], false | undefined>;
const polling: InfinityQuery = { refetchInterval: 5_000, staleTime: 30_000, retry: false,};This example combines polling, freshness and retry policy.
All fields are optional. Omitted or undefined native fields retain the configured
source defaults. Explicit query fields override those defaults for the loader’s
observers without changing the host
QueryClient’s global defaults.
Loading and source options
Section titled “Loading and source options”| Field | Type / default | Usage |
|---|---|---|
source | Scope returned by context.source(...); private context by default | Explicitly share cached reads, a concurrency budget and accepted-write invalidation between views. |
resource | string; 'rows' | Namespace results within a source. Use distinct resources for non-interchangeable datasets. |
consistency | Eventual or snapshot configuration | Defaults to { mode: 'eventual', reload: 'sequence' } for pagination/infinity and reload: 'route' for grouping. See consistency. |
reconcile | (result: Result) => Result; no callback by default | Merge application-owned pending edits into a detached copy of the original loader result. See reconciliation. |
onStatus | (status) => void; no callback by default | Receive pending/success/error, fetching/paused flags and the current error for individual retained reads. |
Native read options and defaults
Section titled “Native read options and defaults”These fields use the installed TanStack Query Core types, including native function-valued forms. The defaults below are the built-in Pro profile; an explicit shared source can supply different base defaults.
| Field | Accepted values | Built-in default |
|---|---|---|
enabled | Boolean or native Query callback returning a boolean | true |
staleTime | Milliseconds, Infinity, 'static', or native Query callback | 30_000 |
gcTime | Milliseconds or Infinity | 60_000 after the last observer leaves |
refetchInterval | Milliseconds, false, or native Query callback returning milliseconds / false / undefined | false |
refetchIntervalInBackground | Boolean | false |
refetchOnMount | Boolean, 'always', or native Query callback | true when stale |
refetchOnWindowFocus | Boolean, 'always', or native Query callback | false |
refetchOnReconnect | Boolean, 'always', or native Query callback | true when stale |
retry | Boolean, retry count, or (failureCount, error) => boolean | Up to two retries for classified transient failures |
retryDelay | Milliseconds or (retryAttempt, error) => number | 500 |
retryOnMount | Boolean or native Query callback | TanStack default true, unless set by the host client |
networkMode | 'online', 'always', 'offlineFirst' | 'online' |
meta | Record of metadata attached to the native query | Unset, unless supplied by the host client |
The default retry classifier accepts HTTP 408, 429 and 5xx errors, or an error
with retryable: true. Other errors are not retried by this default. Reject failed
HTTP responses in your loader; fetch() alone does not reject HTTP errors. An
explicit retry option replaces the classifier with the native retry policy.
gcTime is inactive query retention, not a byte limit or a refresh interval.
staleTime: Infinity does not stop polling. Native enabled: false and 'static'
freshness suppress the automatic refetch paths defined by TanStack; accepted-write
checks still reject obsolete results.
Pages, blocks, routes and observers
Section titled “Pages, blocks, routes and observers”A page is the slice selected by pagination. A block is an offset range
requested by infinite loading. A route is a grouping path such as
['Germany', 'Swimming']; its blocks contain that group’s direct children.
Refetching means running the existing loader again for the same request and
projecting the new result into the grid.
An observer is a live subscription to one cached query. A grid can observe several blocks or routes at once. Retained reads still have observers and can refresh automatically; cached inactive reads have no observers. A QueryClient owns the query cache and coordinates observers. An in-flight read is a loader attempt whose promise has not settled. See shared scopes and concurrency and observer cleanup.
Polling and automatic refetch
Section titled “Polling and automatic refetch”refetchInterval repeatedly reloads retained data after the specified milliseconds.
Use it when the server changes independently of the current user. false stops
the timer; a function can choose an interval or stop based on the native query.
refetchIntervalInBackground permits interval reads while the document is hidden.
By default, hidden documents do not poll. Each observer owns its timer; see
shared in-flight deduplication.
refetchOnMount applies when a query observer subscribes, such as a page or block
being mounted again. refetchOnWindowFocus applies when the document becomes
visible again. refetchOnReconnect applies when the browser reports it is online
again. true refreshes stale data, false disables that trigger, and 'always'
also refreshes fresh data, except queries using 'static' freshness. Native
callbacks can choose the policy. These triggers refresh retained requests; they
do not load all server rows. See freshness.
Freshness and cache retention
Section titled “Freshness and cache retention”staleTime says how long successful cached data is considered fresh. Once it
is stale, a configured mount, focus or reconnect trigger can refetch it.
Becoming stale alone does not start a request. 0 makes successful data stale
immediately; Infinity keeps it fresh until invalidated. 'static' suppresses
the native stale-driven automatic refetch paths, including 'always' triggers.
Polling has its own timer; see polling and refetch.
gcTime controls how long an inactive query remains in the cache after its last
observer leaves. A later matching read can reuse retained cached data according
to its freshness policy. Infinity disables timed cache removal. This is a time
limit, not a row or byte limit; the plugins separately decide which rows and
observers to retain. See pages, blocks and observers.
Enabling reads and network modes
Section titled “Enabling reads and network modes”enabled: false pauses automatic execution for that observer. Use it while
required application inputs are unavailable; enable it through a new loader
configuration when they are ready. A callback can decide from the native query.
Disabling reads does not mean the server data has been deleted or saved.
networkMode: 'online' pauses attempts while the browser reports it is offline.
'always' runs attempts regardless of connectivity, useful for local transports.
'offlineFirst' permits an initial attempt, useful with an offline cache, then
pauses retries while offline. A paused read is waiting for connectivity; it is
different from a terminal error. See status reporting
and reconnect refetch.
Failures and retries
Section titled “Failures and retries”retry controls additional attempts after a loader rejects: false or 0 stops
retrying, a number bounds retries, true keeps retrying, and a function decides
from the failure count and error. retryDelay sets the delay in milliseconds
between attempts; its function form can implement backoff. retryOnMount
controls another attempt when an observer mounts over a cached error.
The built-in classifier and defaults are listed in native read options. Your loader must reject failed HTTP responses explicitly. Retries repeat reads; they do not save edits. See accepted writes for the write boundary.
Metadata and callback policies
Section titled “Metadata and callback policies”meta attaches application metadata to the native query. It can label queries or
provide input to policy callbacks; it does not become a backend request parameter
or a cache identity field. Put backend request inputs in the loader request and
use source/resource identity to separate datasets.
Native query data is opaque; use reconciliation to
work with typed loader results.
Function-valued options
Section titled “Function-valued options”Native functions receive the native Query object, with exceptions for retry callbacks
that receive the arguments shown in failures and retries. The native query cache stores an engine wrapper around loader results to enforce
acceptance fences, so its data type is opaque. Use status/metadata
for native policies, and reconcile for typed product results:
const query: InfinityQuery = { meta: { live: true }, refetchInterval: nativeQuery => nativeQuery.meta?.live ? 5_000 : false, retry: (failureCount, error) => failureCount < 2, retryDelay: attempt => Math.min(1_000 * 2 ** attempt, 30_000),};Keys/hash functions, queryFn, initial/placeholder data, select, structural
sharing and notification suppression remain engine-owned. They are excluded from
this configuration and ignored by runtime forwarding. Keep transport logic in
loadData, loadRows or loadExpandedGroups.
Share reads between views
Section titled “Share reads between views”A context owns loading infrastructure and its lifetime. A source identifies
a backend dataset within that context, including tenant and permission boundaries.
The optional resource is a further namespace inside the source: keep 'rows'
for interchangeable row reads, or choose different names for incompatible results.
Sharing means equal requests can reuse cached data and one in-flight promise.
The concurrency budget limits simultaneous physical attempts across all views;
it is separate from a polling interval or cache retention.
Create one context per app/session, or one per SSR request. Supply the same source to views that use interchangeable backend data and request semantics:
import { createDataQueryContext } from '@revolist/revogrid-pro';
const context = createDataQueryContext();const source = context.source({ sourceId: 'orders-api', schemaId: 'orders-v1', tenantId: 'tenant-a', authorizationScope: 'orders-read',}, { concurrency: 4 });
// gridA/gridB are existing infinity grids with the same backend loader.for (const grid of [gridA, gridB]) { grid.infinityScroll = { ...grid.infinityScroll, query: { ...grid.infinityScroll?.query, source, refetchInterval: 5_000 }, };}sourceId labels the backend, schemaId labels the request/result contract,
tenantId separates tenant data, and authorizationScope labels the permission
boundary. These are non-secret identities, not transport credentials.
Never put credentials in authorizationScope. Use a new scope when permissions,
tenant or backend semantics change. Unrelated loaders get private contexts by
default and do not accidentally share results.
Equal in-flight reads within a shared source/client scope coalesce. Each observer
still has its own polling timer, so completed, separately scheduled ticks can
produce separate sequential reads. Different offsets/routes remain independent.
The source concurrency limit covers all physical attempts, including retries,
across views. With an explicit shared source its concurrency setting takes
precedence over a grouping plugin’s per-model maxConcurrentRequests default.
Views receive detached row objects, so editing A cannot mutate B’s cached projection.
The context may accept an existing QueryClient. Its cache remains owned by the host.
If several contexts use the same client and source identity, source concurrency
and base source policy must match. Use per-loader query options for observer-level
customization. Do not create a new app context on every component render.
External changes and accepted writes
Section titled “External changes and accepted writes”For write execution, unknown-outcome recovery, conflicts and idempotent retry, see Server Writes and Command Receipts.
Invalidation marks cached results stale so active enabled reads can refresh. An accepted write is a change your server has already confirmed. An acceptance fence records that boundary so an older outstanding read cannot overwrite the accepted state. A canonical change is the server-confirmed version of the change; a proposal is an application-owned edit still awaiting acceptance. See pending edit reconciliation.
// For an external server notification, including a WebSocket/SSE message:await source.invalidate();
// Apply a server-confirmed canonical change and reconcile local proposals first:await source.accepted();invalidate() marks source results stale and refetches active enabled reads under
their native policy. accepted() additionally advances an acceptance fence and
cancels older queries, preventing pre-acceptance responses from hydrating a view.
It does not execute a save or establish a server-side snapshot. Where replicas can
lag, your backend must also return data at least as recent as the accepted revision.
Consistency and server ordering
Section titled “Consistency and server ordering”Eventual consistency allows successive reads to observe different server
versions and requires reloads after structural changes. reload: 'sequence'
reloads the retained offset sequence; reload: 'route' scopes reloads to grouped
routes. A snapshot pins reads to one backend revision so blocks and counts
describe the same dataset version. The backend must provide that capability.
A tie-breaker is a unique final sort field, such as a row ID, that makes equal
sort values resolve in a repeatable order.
Offset providers need deterministic ordering with a unique tie-breaker. The default eventual policy requires invalidation/reload after membership, ordering or grouping changes. Stable row IDs do not prevent offset shifts after inserts or deletes.
For a backend that supports pinned snapshots, configure one revision across data, counts and totals:
const snapshot: InfinityQuery = { consistency: { mode: 'snapshot', revision: 'orders-r42', getRevision: result => { // The callback receives the original array or object result shape. if (Array.isArray(result) || !('revision' in result) || typeof result.revision !== 'string') { throw new Error('The backend must return a snapshot revision'); } return result.revision; }, },};The provider must send/pin that revision on every request and return its actual
revision as response metadata. getRevision validates/extracts that server value. A mismatch rejects the result before caching. Apply a new normal
configuration to switch snapshot revision.
Pending edits and status
Section titled “Pending edits and status”Reconciliation merges application-owned pending edits into incoming server results before displaying them. A detached result is a copy isolated from the shared cache and other grids, so this merge cannot edit another view’s data. Hydration means applying the result to the grid; silent hydration does not turn a server refresh into another save or history entry.
Reconciliation runs on a detached loader result, outside the shared cache. It must preserve the original array/object shape, count fields and product semantics. Application-owned proposals remain in the application:
const pending = new Map<number, Partial<Order>>();const query: InfinityQuery = { reconcile(result) { const merge = (row: Order) => ({ ...row, ...pending.get(row.id) }); return Array.isArray(result) ? result.map(merge) : { ...result, data: result.data.map(merge) }; }, onStatus(status) { // Called for individual retained reads; several blocks can be fetching at once. console.log(status.status, status.fetching, status.paused, status.error); },};status.status is 'pending', 'success' or 'error'; fetching and paused are
booleans, and error is the current error value. onStatus is not an aggregate
whole-grid loading counter. Background failures preserve successful same-request
rows. Grouping reconciliation receives raw grouping/bulk results, not internal tags.
Remote hydration is silent and does not create another save or history operation.
Loader parameters and cancellation
Section titled “Loader parameters and cancellation”signal is an AbortSignal: a cancellation notification supplied by the loading
engine. Forward it to fetch so the browser can cancel a request when its last
subscriber leaves. Pagination and infinity supply it inside their optional tenth
argument, context; context?.signal safely reads it when that context exists:
// Call this helper from loadData(..., context).async function fetchOrders(context?: { readonly signal: AbortSignal }) { const response = await fetch('/api/orders', { signal: context?.signal }); return response.json();}Grouping supplies the signal directly as its second callback argument, so its
fetch options use { signal }. This signal belongs to the shared physical read,
not one grid. See destruction and shared request lifetime
for when it aborts and how late responses are handled.
Pagination, infinity scroll and remote trees use this positional order. You can continue to declare only those parameters your existing callback needs:
| Position | Pagination / infinity parameter | Meaning |
|---|---|---|
| 1 | skip | Remote row offset. |
| 2 | take / limit | Requested page or block size. |
| 3 | order | Remote sorting state. |
| 4 | singleConditionFilters | Compatibility model containing the first condition per column. |
| 5 | multiConditionFilters | Complete same-column advanced filter model. |
| 6 | quickFilter | Global quick-filter payload. |
| 7 | temporalContext | Resolved temporal filter semantics. |
| 8 | filterAst | Canonical advanced-filter tree. |
| 9 | executionContext | Captured clock/timezone for that remote filter execution. |
| 10 | context | Optional { readonly signal: AbortSignal } transport cancellation context. |
Remote trees extend the tenth context with parentId (null for roots) and optional
revision. Their { data, total } result requires an exact direct-child total.
See remote tree loading for the contract.
Positions 3–10 are optional. These offset loaders are invoked with ten arguments,
including undefined slots; callbacks inspecting arguments.length see ten.
Grouping keeps loadRows(request, signal?) and
loadExpandedGroups(request, signal?): its cancellation signal is still the second argument.
Destruction and shared request lifetime
Section titled “Destruction and shared request lifetime”Pagination and infinity preserve their nine existing positional arguments and
append optional { signal } context in position ten. Existing callbacks can keep
their current parameters. Grouping retains its existing second signal argument.
Pass the supplied signal to your transport if you want physical request cancellation;
see how signal and context?.signal work.
Destroying a grid releases its observers and polling timers. Removing A does not abort a read still observed by B. Removing the final subscriber aborts the logical query; providers that ignore cancellation can finish physically, but their obsolete result cannot update destroyed views. Infinity eviction and grouping collapse also release the corresponding observers.
For application-owned shared contexts, destroy/unmount all consuming grids before
calling context.dispose(). Injected QueryClient caches are not cleared by disposal.
There is no process-global SSR context. Keep polling and saving separate: the query
configuration manages reads; domain code owns writes and their acceptance.