Skip to content

Async Formula Functions

Use an async formula function when a mounted cell needs data from an application service. Calls are grid-local, case-insensitive, and must be the top-level formula call.

import { FormulaPlugin } from '@revolist/revogrid-pro';
grid.plugins = [FormulaPlugin];
grid.columns = [
{ prop: 'sku', name: 'SKU' }, // A
{ prop: 'service', name: 'Service' }, // B
{ prop: 'price', name: 'Service price' }, // C
];
grid.formulaWorkbook = { sheetId: 'Orders', rowIdProp: 'id' };
const plugins = await grid.getPlugins();
const formulas = plugins.find(plugin => plugin instanceof FormulaPlugin);
if (!formulas) throw new Error('FormulaPlugin is not mounted');
const disposeServicePrice = formulas.registerAsyncFormulaFunction({
name: 'SERVICEPRICE',
async handler([sku, service], { signal, rawArgs }) {
const response = await fetch(
`/api/prices/${encodeURIComponent(String(sku))}?service=${encodeURIComponent(String(service))}`,
{ signal },
);
if (!response.ok) throw new Error(`Price request failed: ${response.status}`);
console.debug('Authored arguments:', rawArgs);
return (await response.json()).price;
},
});
grid.source = [{
id: 'order-1',
sku: 'B-200',
service: 'priority',
price: '=SERVICEPRICE(A1,B1)',
}];
// For example, dispose when this page is torn down.
window.addEventListener('pagehide', disposeServicePrice, { once: true });

Expected result: C1 first renders the default #LOADING state, then the returned price. The handler receives evaluated values (B-200, priority); rawArgs retains A1 and B1.

The default rejected value is #ERROR. Use mapError when users need a domain-specific state:

const dispose = formulas.registerAsyncFormulaFunction({
name: 'SERVICEPRICE',
handler: async ([sku], { signal }) => loadPrice(String(sku), signal),
mapError: error =>
error instanceof Error && error.message.includes('404') ? '#NOT_FOUND!' : '#SERVICE!',
});

Expected result: a missing SKU displays #NOT_FOUND!; other rejections display #SERVICE!. mapError takes precedence over errorValue.

const onStateChange = (event: CustomEvent) => {
const { name, rowId, status, value, error } = event.detail;
console.log({ name, rowId, status, value, error });
};
grid.addEventListener('asyncformulastatechange', onStateChange);
// Abort and clear cached SERVICEPRICE cells, then recalculate them on render.
formulas.invalidateAsyncFormulaFunction('SERVICEPRICE');
// Teardown
grid.removeEventListener('asyncformulastatechange', onStateChange);
dispose();

Expected result: a call emits pending, then resolved or rejected. Repeated renders of the same stable cell/formula/arguments share one request and cached result; invalidation causes a new request.

The handler’s signal is aborted when its cell arguments or formula change, the function is invalidated/unregistered, or the plugin is destroyed. Pass that signal to fetch or your service client. A stale request’s later resolution is ignored.

async handler([sku], { signal }) {
const response = await fetch(`/api/prices/${sku}`, { signal });
return (await response.json()).price;
}

Expected result: editing A1 from B-200 to A-100 aborts the B-200 request and starts one for A-100; only the current request can update the cell.

Registering VLOOKUP intentionally shadows the synchronous built-in only for this mounted grid and only until its disposer runs. Other grids and calculateFormulaWorkbook retain the built-in implementation.

Async functions receive evaluated scalar arguments plus rawArgs. They cannot be nested inside another expression, and headless calculateFormulaWorkbook does not execute them. Load external service data before a backend calculation.

Previous: Cross-sheet Formulas · Next: Backend Calculation and Persistence · Deeper reference: Formula API