[Guide] Build a Travel Planner - implementation notes
Anyone actually like the duplicate fetch by design approach for complex planners, or does it just create noise?
This guide's posture is duplicate fetching by design. Every panel fetches in render, caches are bypassed by default, and derived itinerary evidence is stored into documents so the UI stays always-ready.
Where have you found duplicate fetch to be useful (freshness), and where does it become noise? What derived structures are you storing (day cards, packing list, reservation index, budget summary)? For mutations, do you do optimistic writes without invalidation, or do you re-fetch explicitly? And for posture: do you keep tabs/overlays in global route state, or per-screen local route state?
Comments (10)
Back to latestWhat's the simplest mental model for 'duplicate fetch by design'?
Is it basically: 'don't share data implicitly, share UI contracts explicitly'?
Yes. Each panel owns its own evidence. If two panels need the same record, both fetch it.
You can still mirror to a vault for convenience, but the baseline is: the UI doesn't depend on invisible dedupe.
Ok, so it's not "fetch twice for fun", it's "don't make panels depend on hidden dedupe".
That framing is a lot less annoying.
The tradeoff I’ve seen: duplicate fetch is great during fast navigation because each panel is self-sufficient.
But I still store derived summaries so we're not paying the cost twice in render (budget totals, indexes, preview strings).
The key is to make the posture visible and bounded.
I like a route key like freshness: always | calm so you can flip the posture per session.
It makes the 'why are we fetching so often' question a route-visible decision.
For derived summaries (e.g. reservation index), I store a stable shape.
If the UI expects { [id]: string }, keep it as that. Don’t store random mixed objects that change per panel.
I still prefer deduped reads for expensive endpoints (pricing, availability).
But I can get behind duplicate fetch for 'shape' endpoints (trip, day plan) because it keeps the route story local.
We hit this in prod: we started with invalidation-heavy mutations and it made the planner feel jittery.
Switching to: optimistic write → mutation → derive summaries → keep moving made the UX calmer.
Same. Especially for checklists/notes: users don't want the whole panel to 'reload' after a small write.
A planner is mostly evidence + posture. Keep it stable.
On mutations, we got the calmest UX by treating the itinerary summary as stored evidence, not something you recompute opportunistically.
ts
function addReservation(doc: any, next: Reservation) {
const prev = doc.read('reservations') as Reservation[];
const reservations = [...prev, next];
doc.write('reservations', reservations);
doc.write('reservationIndex', indexById(reservations));
doc.write('budgetSummary', deriveBudget(reservations));
doc.write('mutationEvidence', { type: 'add-reservation', id: next.id, at: Date.now() });
}
Then you can choose whether to re-fetch in the background, but the UI remains deterministic and the evidence tells you what happened.
Question: do you keep the packing list as derived-only, or allow editing directly?
We stored it derived initially, then introduced a 'packing overrides' doc so users could pin items.
I like the day-level checklist derivation from activities. It's a nice example of stored evidence.
If you later move bookings into their own route segment, the derived checklist is still a stable contract.
Duplicate fetch only stayed sane for us once we made the posture explicit (cache bypass by default) and kept the contract surfaces small. Otherwise you end up debating "did we fetch twice" based on vibes and network panel archaeology.
tsx
import { useRenderFetch } from "react";
export function useTrip(tripId: string) {
return useRenderFetch(`/api/travel/trip/${tripId}`, { cache: "bypass" });
}I like the posture for planners, but it can absolutely hammer rate limits if you apply it everywhere.
We defaulted most sessions to a calmer lane and only flipped to always-fresh while actively editing (and we rendered that lane as route state so people understood the tradeoff).