[Guide] Build a Media Gallery - implementation notes
The Media Gallery guide treats loading as an explicit, inspectable choreography: every panel gets an AwaitBoundary, promises are manual so behavior is deterministic, and derived thumbnails / visible ids are stored so the grid is always "ready" even when the network isn't. I like the consistency, but I'm curious how people keep this from turning into boundary soup as the app grows.
How do you decide which segments deserve their own AwaitBoundary vs sharing a boundary higher up? Do you store derived thumbs/rows as real document keys, or keep those derivations local to the grid component? If you're using manual promises, what do you render as evidence for pending/fulfilled/rejected so debugging isn't guesswork? How do you avoid compare-tray overlays becoming their own routing system?
Comments (24)
Back to latestI shipped something similar and the biggest win was consistency.
If every panel uses the same loading posture, people stop reinventing "spinner logic" per screen.
Storing derived thumbs was the only way the grid stayed snappy on slow devices.
If you compute thumb URLs + crop info on every render, you end up with jank that feels like a network problem.
Counterpoint: boundary soup is real.
We tried wrapping every panel and it turned into "why is this panel loading" puzzles because boundaries overlapped.
Yeah, I think the guide's intent is consistency, not maximal boundaries.
We ended up grouping boundaries around "independent refresh cycles". If two panels always refresh together, they share one boundary.
Manual promises helped us because they forced us to name the states.
We stored a loadPlan derived key (array of steps) and rendered the current step as evidence.
We used one AwaitBoundary per segment that can fail independently: grid, detail, metadata, overlay.
If a segment doesn't have its own failure mode, we don't give it a boundary.
Compare tray overlays are basically component-router motion.
We kept it in global route state and rendered data-overlay="compare" so screenshots had an answer.
A minimal pattern we used for evidence with a manual promise:
ts
type PromiseStatus = 'idle' | 'pending' | 'fulfilled' | 'rejected';
function trackPromise(doc: any, key: string, p: Promise<any>) {
doc.write(`${key}:status`, 'pending');
return p.then(
(v) => (doc.write(`${key}:status`, 'fulfilled'), v),
(e) => (doc.write(`${key}:status`, 'rejected'), Promise.reject(e)),
);
}
Then the shell can render data-grid-status etc.
I liked the guide's idea that "hide loading" is the wrong goal.
When loading is readable, users trust the UI more even if it's slower.
We stored visibleIds in the grid document and it fixed a subtle bug where filter chips and the grid disagreed.
Again: less about perf, more about coherence.
Docs tie-in: the boundary approach reads like the docs' "make it legible" posture.
Related: Performance and Rendering (Best Practices) and Testing and Debugging.
We had one boundary that hid too much: when metadata failed, it looked like the whole page was broken.
Splitting metadata into its own boundary made failure feel localized.
Counterpoint: storing derived thumbs can be brittle if the derivation changes (new sizes, new formats).
We stored a thumb version key and forced regeneration when the version changed.
We did the same with a thumbPolicy string in the doc.
If the policy changed, we treated it like a new identity and bumped the grid key to re-baseline.
Manual promises felt heavy at first, but it removed so many "double fetch" arguments because the app said what it was doing.
When state is explicit, people stop projecting meaning onto timing.
We used a derived gridRows key and it made virtualization easier because the row shape was stable.
If you compute rows in render, virtualization can fight you because the structure changes too often.
One misunderstanding I had: I thought AwaitBoundary was just a spinner wrapper.
In the guide it behaves more like a contract: you name what's awaited and what the fallback means.
We prevented the compare tray from being a second router by making it route-visible and dumb.
Overlay state chose *what* is shown; selection state stayed in the document so it didn't leak across routes.
Tiny implementation note: render a single "loading summary" line for the route (grid=pending detail=fulfilled meta=error).
It's ugly but it's the best debugging surface we've found.
We tried putting thumb derivation into an effect and it was a mistake.
The grid would render "wrong" then correct itself. Derive in the mutation path or in a render-visible helper, not after.
We also cached manual promises by key and stored the promise id in the doc as evidence.
It prevented the same segment from starting multiple awaits under subtle rerender conditions.
If you're worried about boundary soup, start with the grid boundary only.
Once the grid contract is stable, adding detail/metadata boundaries becomes an incremental decision.
The guide also convinced me that "loading" is a UX feature you can design, not an accident.
Boundaries + evidence let you actually design it.
If you store derived thumbs, don't forget invalidation-by-version (thumb policy).
Otherwise you get weird mismatches after changing image sizes in prod.
We put a small "promise lane" table in our debug panel (key, status, startedAt).
It turned async bugs into normal bugs.
This guide is the first time I've seen loading treated as a consistent contract rather than a per-component afterthought.