Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Reactive Data - Lona Docs Log in

Reactive Data

The SDK supports reactive patterns for subscribing to sheet changes and keeping your application in sync.

Fetching cell data

SheetHandle manages the cache of cell data for a sheet. Before reading cells, populate the cache for the date range you need:

import { NaiveDate } from "@tento-chrono";

const sheet = await client.open(sheetKey);
const range = new NaiveDate.Range(
  NaiveDate.fromYmd1(2026, 1, 1),
  NaiveDate.fromYmd1(2026, 3, 31),
);
await sheet.cells.fetch(range);

This loads cell values for all rows within the specified range. Subsequent reads are synchronous from cache.

Subscribing to changes

Sheet-level changes

Subscribe to any cell data change in the sheet. The callback receives the set of row IDs whose data changed:

const unsubscribe = sheet.cells.onChange((staleRowIds) => {
  for (const rowId of staleRowIds) {
    console.log(`Row ${rowId} data changed`);
    // Re-read cells or refresh your UI
  }
});

// Later:
unsubscribe();

Per-cell progressive loading

For rows that sync from external providers (calendars, weather), use row.cell(date) for progressive loading with three callback phases:

const row = sheet.row({ label: "Weather" });
const cell = row.cell(NaiveDate.Partial.fromDateAndType(
  "day",
  NaiveDate.fromYmd1(2026, 2, 4),
));

// Show a loading state while data is being fetched
cell.onLoading(() => {
  showShimmer();
});

// Render data when it arrives (may re-fire on sync updates)
cell.onData((data) => {
  render(data);
});

// Clean up when done
cell.dispose();

CellHandle is also awaitable for simple use cases — it resolves when sync completes:

const data = await row.cell(NaiveDate.Partial.fromDateAndType(
  "day",
  NaiveDate.fromYmd1(2026, 2, 4),
));
console.log(data); // CellEntry | null

Sync state

Rows connected to external data sources (calendars, weather) sync asynchronously. Sync progress is tracked per-row and displayed in the sheet legend as a loading indicator. The onData callback fires whenever data changes — including intermediate updates during sync — so your UI stays current without needing to track sync state directly.

For plugins that only need final data (after all sync completes), use onDataFull:

cell.onDataFull((data) => {
  // Only fires when sync is fully complete
  renderFinal(data);
});

Multi-stage sync

Some providers sync in stages. Weather, for example, uses three stages:

StageDescription
viewportData for the currently visible date range
near±3 months around today
fullFull historical (1 year) + forecast (300 days)

Each stage produces a separate sync key with an @stage suffix. The sheet polls each key independently and clears the loading indicator only when all stages reach a terminal state.

Sync dedup

The backend deduplicates sync polls at two levels:

  1. Terminal keys — if a sync key already completed or failed, it is not re-polled on subsequent fetches. This prevents scrolling from restarting sync for already-loaded data.
  2. Active keys — if a poll is already running for a key, a second fetch for the same key does not start a duplicate poll.

Legend loading indicator

The sheet legend shows a loading indicator when a row has active sync. This is driven by RowHandle.onSyncStateChanged, which fires whenever setSyncState is called — including from the poll loop. When all sync keys complete, setSyncState(null) clears the indicator.

Cached reads

After fetching, read cells synchronously:

await sheet.cells.fetch(range);

// Synchronous read from cache
const row = sheet.row({ label: "Weight" });
const cells = sheet.cells.getRange(row.id);
for (const cell of cells) {
  console.log(cell.date, cell.data);
}

See also

  • Rows — row types and tree navigation
  • Cells — reading, writing, and progressive cell loading
  • Data Series — external data sync pipeline