[Guide] Build a Book Library - implementation notes
The Book Library guide is the clearest "scope is the app's configuration surface" argument in the guides so far. Instead of threading policy/theme/safe-mode through props, it treats useScope as a route contract so the whole tree shares one baseline. On top of that, it leans into stored derived bundles (visibleBookIds, resultCount, progressBundle, tocPreview) so lists and reader navigation are always a cheap read.
How do you decide what belongs in scope vs in route state vs in a screen document? Did you store derived visibility lists like visibleBookIds, or keep filters live in render? How did you make scope changes legible (e.g. safe mode or theme) so debugging doesn't turn into "who set this?"? If you shipped a reader, did you store derived navigation plans (next chapter) or compute them on demand?
Comments (22)
Back to latestPutting theme + safe-mode into scope made the whole app calmer for us.
It also stopped people from inventing three different "policy" objects per route.
We treated scope as "configuration" and route as "motion".
If it changes on navigation, it's probably route state. If it changes by deployment/user setting, it's probably scope.
We stored visibleBookIds and resultCount because the shelf header, the grid, and the pagination controls all depended on them.
Otherwise you get that subtle mismatch where the header says 32 results but the grid renders 31 due to a slightly different filter.
Counterpoint: scope can turn into a dumping ground if it's too easy to add keys.
If every feature adds a scope flag, you end up with a global bag of switches nobody can remove.
That's fair. We treated scope keys like public API: versioned, documented, and rendered as evidence.
If a key isn't legible in the UI or logs, it doesn't belong in scope.
Making scope legible: we rendered a one-line banner in debug mode: theme=warm safe=true shareLinks=true.
It felt like "extra", but it cut debugging time in half.
I liked the guide's posture that scope is a contract for the whole tree, not a convenience to avoid props.
It reads like: you are choosing a shared baseline so the UI can't drift into contradictory policy states.
We stored a derived "next chapter plan" because the reader UI needed instant navigation even while metadata refreshed.
If you compute on demand, you get the weird feeling where next/prev buttons lag or change order as reads complete.
Implementation sketch for a scope baseline that stays visible in the DOM:
tsx
export const LibraryScope = createScope({ theme: 'warm', safeMode: true, policy: { allowShareLinks: true } });
export function LibraryShell({ children }: { children: React.ReactNode }) {
const route = useGlobalRoute('library', { density: 'comfortable', overlay: null });
const scope = useScope(LibraryScope);
return (
<section data-theme={scope.theme} data-safe={String(scope.safeMode)} data-density={route.read('density')}>
{children}
</section>
);
}
Even if users never see it, the data-* evidence matters for debugging and screenshots.
I made the mistake of putting query/filter into scope once.
It felt "shared" but it was actually motion, and it created bugs where the query persisted across routes unexpectedly.
On route vs document: we put shelf query + selectedTags in the shelf document, and mode/density in global route.
That matched how we expect back/forward to behave (mode is shared motion, query is local truth).
The guide's "competing stores" line (vault + documents + DOM truth) is provocative, but it matches reality.
Trying to force one store for everything makes readers feel sluggish because every tiny interaction goes through the same pipeline.
Docs connection: scope as contract feels like the same category as route shells: define the baseline, render evidence, keep motion explicit.
Related: Core Concepts (Best Practices) and Routing and Navigation.
Counterpoint to storing visibleBookIds: if your filter is trivial, storing can be more work than it's worth.
But once you have grouping + counts + pagination, storing is the only way to keep siblings consistent.
We used a derived progressBundle that included percent, current chapter title, and a formatted location string.
It let the reader header, footer, and sidebar stay consistent without recomputing in three places.
If you want to keep derivations from being forgotten, centralize them in one helper and call it from mutation paths.
We stopped sprinkling derive() calls across components and made it a document method.
A small win: we rendered a "scope checksum" string as evidence so screenshots could distinguish "warm theme + safe" from "cool theme + unsafe" quickly.
The checksum was just a joined list of keys but it made debugging faster.
Has anyone put policy objects in scope and then needed per-route overrides?
We ended up with a merged policy (scope baseline + route local) and it felt messy.
We did overrides by storing a route-local policy delta and rendering both baseline + delta as evidence.
If you hide the delta, it becomes impossible to reason about why a feature is disabled on one route.
The guide convinced me that scope is more like an env var than like state: it should be stable, legible, and boring.
If it changes every minute, it probably isn't scope.
We used scope to control whether external images render in the reader (policy flag).
Rendering that as data-external-images=false saved us in security reviews because the contract was obvious.
The guide's take that "a single global truth" is not a goal resonates.
Readers are interactive: DOM truth (selection), document truth (progress), and scope truth (policy) all matter.
If you're new to scope, I'd recommend starting with one or two keys that you can render as evidence.
Once you see it working, the rest of the app can align around that baseline.
The biggest benefit was predictability: when scope is a contract, the UI can't quietly diverge route by route.