02examples

Lazy-load by date range

A controlled calendar that only holds what onRangeChange has fetched, merged page by page, with edits written back to the server copy.

The server holds about 300 events spread over sixteen weeks. The calendar holds none of them until it asks: on mount it reports the visible range and the page arrives 350 ms later. Step with ‹ › or switch to month view and watch the status line — every navigation is a request, the count of loaded events only ever grows, and a week you have already seen is drawn from memory while its refresh is in flight. Drag something, leave, come back: the edit is still there, because it was written through to the server.

liveLazy-load by date rangeonRangeChangerangePaddingeventsonEventsChange
loading

How it works

  • events and onEventsChange make the calendar controlled: it draws exactly the array it is given and reports every committed change as a full next array. There are no seeds, no built-in persistence and no store of its own. See Controlled state.
  • onRangeChange(start, end) is the read signal. It fires once on mount and then on every view or date change, with the visible range as [start, end). rangePadding={7} widens what it reports by a week on each side, so the fetch for this week already covers the next one and stepping into it shows no gap. EventServer.fetchRange filters on overlap — an event touching the range is in the page, which is how a cross-midnight event at the edge still appears.
  • Pages merge, they do not replace. mergeById folds a page into what is already loaded, keyed by id, so the neighbours you scrolled past stay put and a refetch of a page you edited returns the edit. Requests can complete out of order and the result is the same.
  • Writes go through the server copy. onEventsChange is only ever handed the loaded pages, so a plain overwrite would drop everything off-screen — EventServer.sync diffs the previous and next arrays instead: an id that vanished is a delete, everything else is an upsert.
  • The status line is host code. requests records each range with a count that stays null while the promise is pending; the live dot reads that. Nothing in kloq knows a request happened.

Take it further

  • Replace fetchRange with a route handler that takes ?from=&to= and returns CalEvent[]; the merge stays. Pass the two dates through toDateOnlyString if the server thinks in days.
  • Use onCommit as the write path instead of the diff. It hands you each change with its type, so a drag is a PATCH and a paste is one batched POST — and a rejection springs the block back.
  • Evict. mergeById never forgets; a long session in day view accumulates. Drop events outside the last few reported ranges and the memory stays flat.

on this page