Loading Errors and Retry
A read error means Kanban could not fetch cards. A save error means your application could not confirm a write. The recovery action depends on which operation failed.
Recover from a failed read
The demo backend can fail one HTTP read and then accept a retry.
Try it: Choose Fail next read, then Retry. Repeat to confirm recovery works each time.
HTTP reads: 0 · writes: 0
What to notice: The error is visible, retries use the normal loader, and the backend data remains intact.
TypeScript source
The supplied backend handlers use MSW simulation in the browser. Replace the transport with your application endpoint.
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 Fail next read, read the error, then Retry to recover. This example’s Retry control restarts from page zero. The demonstration disables automatic read retries so the failure is visible.
Retry a read
Section titled “Retry a read”Failed pages remain retryable and ordinary page failures preserve already loaded cards. Use the built-in retry action to repeat the failed page while preserving other loaded positions. From an application control, refreshRemote() restarts at page zero and clears history; indexed mode rebuilds its placeholders. loadMoreCards() requests the next page and is not a generic retry for an arbitrary failed indexed slot.
const state = kanban.getRemoteState();if (state.error) { await kanban.refreshRemote();}Obtain kanban using Get the plugin. Show application feedback through the remote loading events.
Configure read retries
Section titled “Configure read retries”Set remote.query.retry for automatic retries of failed reads. The fixture uses false; a production app may choose a bounded retry policy. Forward the loader’s abort signal and release application-owned contexts when the view unmounts.
Do not blindly replay a save
Section titled “Do not blindly replay a save”A timeout does not establish that the server rejected the move. Your app should show pending/uncertain state, prevent conflicting follow-up edits and reconcile with the server. The optional managed adapter has its own explicit retry/conflict workflow for complete datasets. Browser read caching does not provide write idempotency.
Next steps
Section titled “Next steps”See loading, saving and cache policy. See the Kanban API for exact types.