Skip to content

Custom Formula Functions: Sync & Async

This demo defines two custom formula functions and compares React, Vue, and Svelte using live GitHub data:

  • GH_REPO(repo) — async (BaseFunctionAsync). Fetches stars, forks, open issues, size, and subscribers from the GitHub API and spills them across columns B–F. Cells show a loading animation while the request is in flight.
  • REPO_OWNER(repo) — sync (BaseFunction). Extracts the owner from an owner/repo slug in column G, instantly.

The Total row uses SUM; it stays pending until every async cell resolves.

Debugger (inspect async evaluation & caching)
Registry State
1{}
Cell:
No cell data
System:
No system data
Formula Expressions
No cell selected
No sheets detected
Sheet Data
Sheet instance not found
Store Data
Store state not found

💡 GH_REPO(repo) (async) makes one API call per row and spills [[stars, forks, issues, size, subscribers]] across B–F — 3 calls instead of 15. Results are cached for 1 minute. REPO_OWNER(repo) (sync) in column G resolves instantly. Edit a repository name in column A to refetch; the Total row stays pending until every async cell resolves.

📄 View Source Code

  • Use BaseFunction when the result can be computed immediately from the arguments (string/number transforms, validation, lookups in memory). main returns the value directly.
  • Use BaseFunctionAsync when the result needs I/O (HTTP requests, IndexedDB, workers). main returns a Promise; GridSheet tracks the pending state and re-renders the cell when it resolves.
import { BaseFunction, ensureString } from '@gridsheet/react-core';
class RepoOwnerFunction extends BaseFunction {
example = 'REPO_OWNER("facebook/react")';
defs = [{ name: 'repo', description: '"owner/repo" or a github.com URL', acceptedTypes: ['string'] }];
main(repo: string) {
const slug = ensureString(repo)
.trim()
.replace(/^https?:\/\/github\.com\//, '');
const owner = slug.split('/')[0];
if (!owner) {
throw new Error(`Invalid repository: "${repo}"`);
}
return owner;
}
}

A thrown error is shown as an error in the cell.

import { BaseFunctionAsync, ensureString } from '@gridsheet/react-core';
class GhRepoFunction extends BaseFunctionAsync {
example = 'GH_REPO("facebook/react")';
defs = [{ name: 'repo', description: '"owner/repo" format', acceptedTypes: ['string'] }];
ttlMilliseconds = 60 * 1000; // cache result for 1 minute
protected autoSpilling = true; // spill the returned 2D array
protected broadcastDisabled = true; // treat the argument as a single value
async main(repo: string) {
const r = ensureString(repo).trim();
const resp = await fetch(`https://api.github.com/repos/${encodeURI(r)}`, {
headers: { Accept: 'application/vnd.github.v3+json' },
});
if (!resp.ok) {
throw new Error(`GitHub API error for ${r}: ${resp.status}`);
}
const data = await resp.json();
return [[data.stargazers_count, data.forks_count, data.open_issues, data.size, data.subscribers_count]];
}
}

Register both via additionalFunctions in useSpellbook. The formula name is the key uppercased:

const book = useSpellbook({
additionalFunctions: {
gh_repo: GhRepoFunction, // =GH_REPO(A1)
repo_owner: RepoOwnerFunction, // =REPO_OWNER(A1)
},
});

With autoSpilling = true, the 2D array returned by main (or resolved by its Promise) is wrapped in a Spilling automatically and overflows into adjacent cells. Without the flag, you can return new Spilling(matrix) yourself (Spilling is exported from @gridsheet/react-core) — but for an async function prefer autoSpilling, which wraps the resolved matrix.

In this demo, =GH_REPO(A1) in B1 fills B1:F1 with a single API call — 5 values per row, 3 repos, so 3 API calls instead of 15.

When an async cell is still resolving, GridSheet marks it as Pending. Any formula that depends on a Pending cell (e.g., =SUM(B1:B3)) automatically waits and displays the loading animation until all upstream cells settle.

In this demo, the Total row uses =SUM(B1:B3) for each column. It stays pending until all three GH_REPO calls complete.

Built-in Result Caching via ttlMilliseconds

Section titled “Built-in Result Caching via ttlMilliseconds”

Set ttlMilliseconds on the function class to enable automatic result caching. The same arguments will return the cached result without making a new network request until the TTL expires.

ttlMilliseconds = 60 * 1000; // cache for 60 seconds

This prevents redundant API calls when a formula is re-evaluated due to an unrelated cell change.

Making Cells Editable to Trigger Re-fetches

Section titled “Making Cells Editable to Trigger Re-fetches”

Because the repo slug in column A is a plain string value, the user can edit it directly in the grid. When the value changes, both =GH_REPO(A1) and =REPO_OWNER(A1) are re-evaluated — the sync one immediately, the async one after its fetch.

@gridsheet/react-dev exports a Debugger component that visualizes the current book state — pending cells, resolved values, and formula dependency graphs. It is useful during development to inspect async resolution.

import { Debugger } from '@gridsheet/react-dev';
<GridSheet book={book} ... />
<Debugger book={book} />