Skip to content

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 configurationExisting loaderPublic configuration type
grid.paginationloadData(skip, take, ...)PaginationConfig
grid.infinityScrollloadData(skip, limit, ...)InfinityScrollConfig<Row>
grid.tree.remoteloadData(skip, limit, ..., context)TreeRemoteConfig<Row>
grid.serverSideGroupingloadRows(request, signal?), optional loadExpandedGroups(request, signal?)ServerSideGroupingConfig<Row>

Pagination without loadData remains local pagination. Query options apply to remote reads.

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.

ConfigurationBehavior
Omit query, or use a query options objectBuilt-in Query engine
query: { enabled: false }Query remains configured; automatic reads pause
query: falseExecute 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.

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.

FieldType / defaultUsage
sourceScope returned by context.source(...); private context by defaultExplicitly share cached reads, a concurrency budget and accepted-write invalidation between views.
resourcestring; 'rows'Namespace results within a source. Use distinct resources for non-interchangeable datasets.
consistencyEventual or snapshot configurationDefaults to { mode: 'eventual', reload: 'sequence' } for pagination/infinity and reload: 'route' for grouping. See consistency.
reconcile(result: Result) => Result; no callback by defaultMerge application-owned pending edits into a detached copy of the original loader result. See reconciliation.
onStatus(status) => void; no callback by defaultReceive pending/success/error, fetching/paused flags and the current error for individual retained reads.

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.

FieldAccepted valuesBuilt-in default
enabledBoolean or native Query callback returning a booleantrue
staleTimeMilliseconds, Infinity, 'static', or native Query callback30_000
gcTimeMilliseconds or Infinity60_000 after the last observer leaves
refetchIntervalMilliseconds, false, or native Query callback returning milliseconds / false / undefinedfalse
refetchIntervalInBackgroundBooleanfalse
refetchOnMountBoolean, 'always', or native Query callbacktrue when stale
refetchOnWindowFocusBoolean, 'always', or native Query callbackfalse
refetchOnReconnectBoolean, 'always', or native Query callbacktrue when stale
retryBoolean, retry count, or (failureCount, error) => booleanUp to two retries for classified transient failures
retryDelayMilliseconds or (retryAttempt, error) => number500
retryOnMountBoolean or native Query callbackTanStack default true, unless set by the host client
networkMode'online', 'always', 'offlineFirst''online'
metaRecord of metadata attached to the native queryUnset, 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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:

PositionPagination / infinity parameterMeaning
1skipRemote row offset.
2take / limitRequested page or block size.
3orderRemote sorting state.
4singleConditionFiltersCompatibility model containing the first condition per column.
5multiConditionFiltersComplete same-column advanced filter model.
6quickFilterGlobal quick-filter payload.
7temporalContextResolved temporal filter semantics.
8filterAstCanonical advanced-filter tree.
9executionContextCaptured clock/timezone for that remote filter execution.
10contextOptional { 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.

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.