Caching and Refresh
A browser cache remembers successful reads so the same page may be reused. It does not save moved cards or enforce server-side permissions.
The read engine is built in. Keep your existing loadData callback and add only the remote.query options you need.
Reuse a fresh server response
This small board loads all 24 cards in one request and uses a 30-second Query freshness window.
Try it: Reopen the same board, then force a server refresh. Compare the HTTP read counter.
HTTP reads: 0 · writes: 0
What to notice: Reopening can reuse fresh Query data; explicit refresh requests current backend data. No extra app cache is implemented.
TypeScript source
The supplied backend handlers use MSW simulation in the browser. Replace the transport with your application endpoint.
import { createDataQueryContext } from '@revolist/revogrid-pro';import { remoteBoard } from './remote-board';
export function sharedRemoteBoard(apiUrl: string) { const context = createDataQueryContext(); const config = remoteBoard(apiUrl); return { config: { ...config, remote: { ...config.remote!, chunkSize: 24, query: { retry: false, staleTime: 30_000, source: context.source({ sourceId: apiUrl, schemaId: 'cards-v1', tenantId: 'demo', authorizationScope: 'demo' }), } } }, destroy() { context.dispose(); }, };}// This small cache example loads all 24 cards at once, keeping paging separate.// Reuse this config/source while reopening the SAME app dataset.// Keep tenant and authorization identity accurate in your application.// Dispose the context when the app no longer owns this shared read scope.import type { KanbanConfig, KanbanCardMoveDetail } from '@revolist/kanban';import type { WorkCard } from './first-board';import { boardConfig, configureBoard } from './first-board';
// apiUrl is YOUR endpoint. The documentation supplies an isolated HTTP demo backend.export function remoteBoard(apiUrl: string): KanbanConfig<WorkCard> { return { ...boardConfig, remote: { chunkSize: 6, query: { retry: false, refetchOnWindowFocus: false }, async loadData(skip, limit, _order, _single, _multi, _quick, _temporal, _ast, _execution, context) { const response = await fetch(`${apiUrl}/cards?skip=${skip}&limit=${limit}`, { signal: context?.signal }); if (!response.ok) throw new Error(`Card loading failed: HTTP ${response.status}`); return response.json(); // { data: Card[], total: number } }, } };}
// Read-only setup; add app-owned write handling before enabling moves.export async function createServerBoard(container: HTMLElement, apiUrl: string) { const grid = document.createElement('revo-grid'); grid.style.cssText = 'display:block;width:100%;height:360px'; container.append(grid); try { // configureBoard waits for grid initialization before activating the remote loader. const kanban = await configureBoard(grid, { ...remoteBoard(apiUrl), readOnly: true }, []); return { grid, kanban, destroy() { grid.remove(); } }; } catch (error) { grid.remove(); throw error; }}
export async function saveMove(apiUrl: string, detail: KanbanCardMoveDetail, signal?: AbortSignal) { const response = await fetch(`${apiUrl}/moves`, { method: 'POST', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ changedCards: detail.changedCards }), }); if (!response.ok) throw new Error(`Move save could not be confirmed: HTTP ${response.status}`); return response.json();}// grid.kanban = remoteBoard(apiUrl);// grid.addEventListener('kanbancardmove', event => { void saveMove(apiUrl, event.detail); });// In your app, pause conflicting moves while saving; on failure refresh before more edits.// Pagination, refresh and Query policy belong to remote. There is no remote.commit method.import { defineCustomElements } from '@revolist/revogrid/loader';import { KanbanPlugin, type KanbanConfig } from '@revolist/kanban';import '@revolist/revogrid-pro/styles.css';import '@revolist/kanban/styles.css';
export interface WorkCard { id: string; title: string; status: string; order: number; description?: string; team?: string; owner?: string; priority?: string; tags?: string[]; progress?: number; startDate?: string; endDate?: string;}export const demoCards: WorkCard[] = [ { id: 'task-1', title: 'Research customer needs', description: 'Collect feedback for the next release.', status: 'todo', team: 'product', owner: 'Ana', priority: 'normal', order: 1000 }, { id: 'task-2', title: 'Build onboarding', description: 'Make the first session useful.', status: 'doing', team: 'product', owner: 'Mia', priority: 'high', order: 1000 }, { id: 'task-3', title: 'Improve API errors', description: 'Return actionable validation messages.', status: 'todo', team: 'platform', owner: 'Rui', priority: 'normal', order: 2000 }, { id: 'task-4', title: 'Release checklist', description: 'Ready for the next deployment.', status: 'done', team: 'platform', owner: 'Sam', priority: 'normal', order: 1000 },];export const boardConfig: KanbanConfig<WorkCard> = { columns: [{ prop: 'todo', name: 'To do', size: 210 }, { prop: 'doing', name: 'In progress', size: 210 }, { prop: 'done', name: 'Done', size: 210 }], card: { titleField: 'title' }, cardRowHeight: 88, addCard: false, swimlaneColumn: false,};
export async function configureBoard( grid: HTMLRevoGridElement, config: KanbanConfig<WorkCard> = boardConfig, cards: readonly WorkCard[] = demoCards, extraPlugins: HTMLRevoGridElement['plugins'] = [],) { await defineCustomElements(); grid.plugins = [KanbanPlugin, ...extraPlugins]; // The packaged editor and history install automatically. grid.columns = [{ prop: 'title', name: 'Title' }, { prop: 'status', name: 'Status' }, { prop: 'owner', name: 'Owner' }]; // Cached remote data can hydrate immediately: finish initial grid setup first. grid.kanban = config.remote ? { ...config, remote: false } : config; grid.source = cards.map(card => ({ ...card })); await grid.componentOnReady(); const plugins = await grid.getPlugins(); const kanban = plugins.find((plugin): plugin is KanbanPlugin<WorkCard> => plugin instanceof KanbanPlugin); if (!kanban) throw new Error('Kanban could not start.'); if (config.remote) grid.kanban = config; return kanban;}
// Give your container a width and mount once after it exists in the DOM.export async function createKanbanBoard(container: HTMLElement) { await defineCustomElements(); const grid = document.createElement('revo-grid'); grid.style.cssText = 'display:block;width:100%;height:400px'; container.append(grid); try { const kanban = await configureBoard(grid); return { grid, kanban, destroy() { grid.remove(); } }; } catch (error) { grid.remove(); throw error; }}import { http, HttpResponse } from 'msw';import type { DemoBackendModule } from '@revolist/revogrid-examples/demo-api-mocks';
export interface ServerCard { id: string; title: string; status: string; order: number; }export function createKanbanDocsStore() { let failNextMove = false; let cards: ServerCard[] = Array.from({ length: 24 }, (_, index) => ({ id: `remote-${index + 1}`, title: `Server task ${index + 1}`, status: ['todo', 'doing', 'done'][index % 3], order: (index + 1) * 1000, })); return { failNextMove() { failNextMove = true; }, page(skip: number, limit: number) { if (!Number.isSafeInteger(skip) || skip < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new Error('Invalid page'); return { data: structuredClone(cards.slice(skip, skip + limit)), total: cards.length }; }, move(changes: unknown) { if (failNextMove) { failNextMove = false; throw new Error('Simulated save unavailable'); } if (!Array.isArray(changes) || !changes.length) throw new Error('Changed cards are required'); const ids = new Set<string>(); for (const card of changes) { if (!card || typeof card.id !== 'string' || ids.has(card.id) || !cards.some(item => item.id === card.id) || !['todo', 'doing', 'done'].includes(card.status) || !Number.isFinite(card.order)) throw new Error('Invalid card changes'); ids.add(card.id); } // Apply one validated move batch atomically, including rank-rebalanced neighbors. cards = cards.map(card => { const change = changes.find(item => item.id === card.id); return change ? { ...card, status: change.status, order: change.order } : card; }); return { saved: ids.size }; }, };}
/** HTTP backend supplied with the docs; isolated data per preview session. */export const kanbanDocsBackend: DemoBackendModule = { id: 'kanban-docs', create({ apiUrl, signal }) { const store = createKanbanDocsStore(); return { handlers: [ http.get(`${apiUrl}/cards`, ({ request }) => { const params = new URL(request.url).searchParams; try { return HttpResponse.json(store.page(Number(params.get('skip') ?? 0), Number(params.get('limit') ?? 6))); } catch { return HttpResponse.json({ message: 'Invalid page' }, { status: 400 }); } }), http.post(`${apiUrl}/control/fail-next-write`, () => { store.failNextMove(); return HttpResponse.json({ armed: true }); }), http.post(`${apiUrl}/moves`, async ({ request }) => { try { const body = await request.json() as { changedCards?: unknown }; signal.throwIfAborted(); request.signal.throwIfAborted(); return HttpResponse.json(store.move(body.changedCards)); } catch (error) { return HttpResponse.json({ message: error instanceof Error ? error.message : 'Invalid move batch' }, { status: error instanceof Error && error.message === 'Simulated save unavailable' ? 503 : 400 }); } }), ] }; },};Try it: choose Reopen same board, then Refresh from server. Compare the HTTP read counter: reopening can reuse the fresh response, while refresh requests authoritative server state.
Start with freshness
Section titled “Start with freshness”grid.kanban = { ...config, remote: { ...remote, query: { staleTime: 30_000, retry: 1 }, },};Here remote is your existing loader configuration. staleTime is the period during which a successful page can be considered fresh. retry applies to failed reads, not to saves.
Call await kanban.refreshRemote() to discard the current sequence or reset indexed placeholders and read page zero freshly. This explicit refresh also clears local history. Native automatic refetch updates loaded positions without recording a local edit and retains the current history stack.
Query integration
Section titled “Query integration”KanbanRemoteConfig.query optionally configures the built-in whole-board read
engine. Existing loadData(skip, limit) callbacks keep working. See
remote card pages for append and
indexed layout contracts and cancellation examples.
| Type / property | Contract |
|---|---|
KanbanRemoteConfig<Card>.query | KanbanRemoteQueryOptions | false; exported by @revolist/kanban, optional, defaults to fresh reads and a private context. |
KanbanRemoteQueryOptions | Omit<LoadQueryOptions<never>, 'consistency' | 'reconcile'>. Native fields from configuration types apply to mounted blocks/sequences. |
query.source | Optional DataQueryScope created through createDataQueryContext from Pro. Share only equivalent dataset/schema/tenant/authorization scopes. |
query: false | Direct manual reads, without query caching or native automatic refetch/retry. |
import type { KanbanRemoteQueryOptions } from '@revolist/kanban';const query: KanbanRemoteQueryOptions = { refetchInterval: 5_000, refetchOnWindowFocus: true, refetchOnReconnect: true, retry: 2,};// Keep existing loaders: remote: { ...existingRemote, query }.Indexed blocks validate every requested position and fixed total before caching,
then hydrate only that block’s canonical slots. Append sequences advance by the
actual short-page length, or one full window for an empty authoritative page.
Returned totals take precedence over continuation flags; explicit flags otherwise
take precedence over configured totals. Native refetch restarts retained pages from
their first offset and recomputes later offsets; explicit refreshRemote() resets
to page zero and forces a fresh read even with staleTime: Infinity or 'static'
and refetchOnMount: false. Retrying an indexed slot reuses its mounted observer;
recovering one slot preserves errors in other slots so they remain retryable.
Automatic updates retain the current history stack and do not add a
local edit entry; returned server cards are authoritative for the loaded positions.
Destroying/disabling/replacing a board’s read scope or adopting an external source releases its observers. Cancelling one board leaves a shared peer’s request alive; last-subscriber cancellation aborts the transport and settles callers even when a loader ignores abort. After all boards are destroyed, dispose application-owned contexts. Remote reads cannot be combined with built-in persistence; that adapter requires a complete canonical dataset. See Kanban writes.
Browser cache and backend cache
Section titled “Browser cache and backend cache”The browser’s built-in Query cache avoids an HTTP read. A cache inside your backend may avoid a database query. They have different owners and invalidation rules. query: false bypasses the browser read engine and its automatic retry/refetch policy; it does not control a backend cache.
Next steps
Section titled “Next steps”See shared loading options, save changes and error recovery. See the Kanban API for exact types.