[Guide] Build a Calendar App - implementation notes
The Calendar guide is an unapologetic argument that editing should be a session route, not a fragile overlay on top of an event id. The "random slug" edit session makes the UI describe *what you are doing* (editing) even while the underlying event is moving, and the rest of the guide doubles down on stored derived structures so the month grid and agenda never depend on last-second computation.
Did the edit session route reduce the "draft drift" bugs for you, or did it just move them into the session doc? How far did you go with stored derived state (grid cells, bundles, grouped events, preview strings) before it became hard to keep derivations up to date? What evidence do you render so a screenshot can explain why a day cell is highlighted and which session slug is active? If you're dealing with timezones/recurrence, do you store derived "view math" or keep the math live in render?
Comments (18)
Back to latestSession routes were the big win for us.
It stopped the editor from re-baselining just because list data refreshed.
Storing a 42-cell grid felt silly until we shipped week switching + agenda reuse.
Once multiple panels rely on the same week bundle, "compute it in render" turns into three slightly different computations.
The guide's "duplicate reads for freshness" line is the part that people argue about.
But in calendar UIs, it really does matter that the agenda is "fresh" even if the month grid is stale for a few seconds.
I initially thought "random slug route" meant the event id stops mattering (like you can't deep link to an event).
But the guide is saying the session route is a stable editing *surface*, not that the resource disappears.
Exactly. We still link to /calendar/event/[eventId] for viewing, but /calendar/edit/[sessionSlug] is a separate contract.
The session doc can *point at* the event id, but it doesn't have to be *named by* it.
That framing helped. It's like a "workspace route" for an edit, not a replacement for resource routes.
We went too far on derived keys at first (every grouping, every formatted string).
What worked was drawing a line: store derived keys that multiple siblings read (grid, week bundle, agenda rows, preview), and compute per-leaf decoration in render.
Concrete thing we stored as evidence: activeSessionSlug rendered into the shell as data-edit-session.
Support could screenshot a bug and we instantly knew if they were editing in the right session.
The month grid derivation in the guide is basically: derive once, store, and render from the stored shape. We kept it as a helper that only touches the document (so you can't "forget" to write the derived keys):
ts
function deriveMonthGrid(doc: any, monthIso: string) {
const grid = build42DayGrid(monthIso);
doc.write('grid', grid);
doc.write('selectedWeek', grid.slice(0, 7));
doc.write('agendaRows', buildAgendaRows(doc.read('events'), doc.read('selectedWeek')));
}
The API is crude, but it makes "grid is stale" a single call site to debug.
Counterpoint: random session slugs can create a cleanup problem.
If users open 8 edit sessions, do you keep them forever? Do you expire? Do you merge?
We expired them (e.g. lastTouchedAt + 30 days) and rendered an "expired session" banner that offers to fork a new session.
It sounds heavy, but it actually made support interactions clearer than "your draft vanished".
Timezones: we stored derived "view math" as strings (day labels, time labels, duration minutes) and treated them as UI helpers.
Trying to keep it live in render caused inconsistencies between the month cell, agenda row, and editor preview.
We used the guide's idea of a route-visible selectedDate and it fixed a bunch of back/forward weirdness.
It also made it easier to build "copy link to this day" without inventing a new route.
The guide made me revisit our routing docs because I didn't realize how much behavior we were hiding in component state.
Docs tie-in: Routing and Navigation and State Management (Best Practices).
We had a bug where the month grid and agenda disagreed about which week was selected because we derived separately.
Storing the selectedWeek bundle in the month document eliminated that entire class of bugs.
Small implementation note: if you're doing Field-style inputs in the editor, the blur/commit boundary matters a lot.
We committed on blur and rendered data-dirty on the editor shell so you could *see* whether draft and preview were aligned.
One thing I didn't expect: session routes made concurrency less scary.
When two people edit the same event, you can treat it as two sessions racing, and render evidence for whose session won.
The part that feels "wrong" to some folks is duplicating reads instead of invalidating.
But when you have an agenda list, a day cell preview, and an editor, it's easier to let each panel choose freshness than to orchestrate invalidation perfectly.
We wrote the session slug to the event as lastEditSessionSlug so we could show "continue editing" in the event detail route.
That small link between routes made the app feel way more coherent.
If you want to generate a session slug deterministically (for tests), we used a tiny helper:
ts
export function newSessionSlug(now = Date.now()) {
return `s-${now.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
Then tests can pass a fixed now and you still get a human-readable slug.
Last thought: rendering evidence in the shell is the only way I've found to keep calendars debuggable.
If you can see panel/date/session in the DOM, a lot of "calendar is haunted" reports become actionable.