Repository navigation
docs(skills): ux-architecture — a recipe for fast, stable navigation with urls() and clientUrls() - #1134
Merged
Conversation
commit: |
8 of 9 tasks
…with urls() and clientUrls() A new skill, packages/rangojs-router/skills/ux-architecture/SKILL.md, for agents that design or review a Rango app's routing, layout, loading and navigation UX. The existing skills are organised by primitive; none says where layouts, loaders, boundaries, revalidate() rules, prefetch and modals go so a click shows the next page at once, nothing on screen remounts, and no fallback re-flashes. - The UX contract in one place: what Rango guarantees and what the app must do to keep it, and how to judge it (a build, a 300-500 ms server). - A decision tree: server urls() pages vs clientUrls() groups, server structure around an include(), cross-group navigation, layout and loader placement with the revalidate() defaults and four predicates, loading() vs inline Suspense, prefetch (prefetch: false and its measured cost), transition(), intercept() and parallel(), actions, the three update channels, handles. - Five recipes: a shop (vite-rsc-demo client-shop as the reference), dashboard tabs, settings forms, search with filters, a modal over a list. - Fifteen anti-patterns, each with the symptom and the fix. - Known gaps: dc-t38 with a measured workaround, dc-t30, React's 300 ms throttle and the other documented React gaps. Wiring: the rango index row, catalog.json, a pointer in docs/README.md, and the skill added to check-docs-api's guarded docs.
… on navigation; revalidate(true|false) shorthand A group layout() loader now takes a server layout's revalidation default (#1137): held on navigation, re-run after actions unless its revalidate() narrows that. The loader-defaults tables, the shop recipe and the cart rule say so, and the known gap is gone. prefetch: false skips only a loader new to the page. The revalidate() anti-patterns use the #1130 shorthand.
ivogt
force-pushed
the
docs/skill-ux-architecture
branch
2 times, most recently
from
October 13, 2026 10:22
cfbf68a to
5643e2a
Compare
… with the generated map) and throw rewrite()
ivogt
force-pushed
the
docs/skill-ux-architecture
branch
from
October 13, 2026 10:26
5643e2a to
6bb2034
Compare
4 tasks done
ivogt
marked this pull request as ready for review
October 13, 2026 11:24
2 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
devcorner: dc-t40 · session dc-s36
docs(skills): ux-architecture — a recipe for fast, stable navigation with urls() and clientUrls()
Problem
An agent building a Rango app reads skills organised by primitive (
/client-urls,/loader,/layout, ...). None of them says how to compose an app so navigation is instant and stable, so agents put shared chrome in route components (it remounts on every route change), wrap a page in one big<Suspense>(the whole page flashes a skeleton), writerevalidate(() => true)or() => falseby habit (wasted runs, or the old product shown after product to product), throw transient loader failures (an action's form is replaced by an error boundary), and split an area into severalclientUrls()groups (every navigation between them is a server round trip). The user: "agents dont know how good to use rango with url() and clientUrls and need reciept on how to architect a good UX".Before / after
Before: an agent asked "where does the cart badge's loader go, and which
revalidate()does it get?" has to assemble the answer from/client-urls,/loader,/layout,/linksanddocs/client-urls.md, and nothing tells it that a grouplayout()loader re-runs on every cross-route navigation today (dc-t38).After:
/ux-architecturesection 2.3 and 2.4 answer it in one place (server layout around theinclude(), the ternaryisAction() ? isAction(CartActions) : undefined, why), recipe 3.1 shows it in a shop, and section 5 states the dc-t38 gap with a measured workaround.The skill's structure (886 lines, about 6,900 words)
urls()page orclientUrls()group. 2.2 server structure around the group, and cross-group navigation (the shell stays, the destination group mounts; chrome that must survive goes above the groups). 2.3 server layout vs grouplayout(). 2.4 loader placement, therevalidate()defaults table and four predicates (bySlug,bySearch,byCartAction,bySavedProfile). 2.5loading()vs inline<Suspense>. 2.6 prefetch,prefetch: falsewith its measured cost,prefetchKey=":source". 2.7transition(),revalidate={false}. 2.8intercept()andparallel(). 2.9 actions and failures as data. 2.10 three update channels (own actions plusrevalidate();load()/refreshGroup/router.refresh()/invalidateClientCache(); cache tags). 2.11 handles.client-shopnamed as the reference implementation), dashboard tabs, a settings area with forms, search with filters, a modal over a list.Verification
Every behavioural claim was checked against origin/main e7b3d88. "Probe" is a scratch
serveShellRequesttest (not committed) of a server layout with two loaders around aninclude()of a group with alayout()(one loaderrevalidate(() => false), one with none) and two routes; counts are loader runs per request.viewportin production,nonein devsrc/router/prefetch-default.ts:35-37src/browser/partial-update.ts:1049-1062,:1170-1206transition()is in scope; group segments are keyedcg:<mount>src/segment-system.tsx:505-569;src/client-urls/server-projection.ts:659layout()keeps its instance across its routes; the route component is keyed by routesrc/client-urls/client-root.tsx:190-208; e2e "a layout() keeps its DOM across navigations between its routes (#1124)" in both apps, dev and productionsrc/client-urls/client-root.tsx:126-144src/client-urls/navigation.ts:93-108,:138-142docs/client-urls.md, "Navigation authority";client-root.tsx:126-138(useDeferredValue)tests/vite-rsc-demo/e2e/client-shop-guards.test.ts(dev +prodDescribe)layout()loader with no rule re-runs on param and search change and after every actionlayoutDefault1 on param nav, search nav, both actionslayout()loader with() => falseruns on a cross-route navlayoutHeld0 on param nav, 1 on/g/items/ato/g;src/client-urls/client-urls.ts:569-597;segment-resolution/revalidation.ts:254outerCart0, 0, 0, 0; 1 on the cart action; 0 on the other actionuseLoaderfinds a loader registered on an enclosing layoutsrc/use-loader.tsx:175-212(walkscurrent.parent),src/outlet-provider.tsx:47(code read, not test-pinned for a read inside a group)?tab=1to?tab=2:itemById0,item1; param prefetch:itemById1falseon actions holds after an actionitemById0 on both actionsskills/loader/SKILL.mddefaults table;docs/internal/execution-model.md, "Revalidation Contract" (first-render guarantee)loading()is awaited; withloading()it streamsdocs/internal/execution-model.md:802-815loading(); forceAwait and action lanes commit wholesrc/segment-system.tsx:705-750,:776-790docs/client-urls.md, "Supported surface";skills/client-urls/SKILL.md, "Revalidation runs on the server"prefetch: falsein a group: the click shows the page at once, one fill; read under its own Suspenseskills/client-urls/SKILL.md, "prefetch: false";docs/client-urls.mdkeepClientCache()src/router/prefetch-cache-ttl.ts,src/router.ts:278;skills/client-urls/SKILL.md, "After an action"prefetchKey=":source"for targets whoserevalidate()readscurrentUrl/currentParamssrc/browser/react/Link.tsx:107-128revalidate={false}andsetSearchParams(..., { revalidate: false })update URL and hooks with no request; same pathname onlysrc/browser/react/Link.tsx:78-86;skills/hooks/urls.md,navigation.mdisLoadingflags the held readerpartial-update.ts:1049-1062;skills/hooks/data.md;tests/vite-rsc-demo/e2e/client-shop-filters.test.tsdocs/internal/execution-model.md, "Revalidation Contract";docs/client-urls.md, "After an action"docs/client-urls.md, "A loader that fails on its rerun after an action" (e2eaction-failurein both apps)tests/vite-rsc-demo/e2e/client-shop-cart.test.ts(dev + production)load()/refetch/useRefreshLoaders()need a fetchable loader; plainload()broadcastsskills/hooks/data.mdinvalidateClientCache()marks history stale and flushes prefetchesskills/hooks/state.mdcacheTag,updateTag,revalidateTag,keepClientCache,invalidateClientCacheexportssrc/index.rsc.ts:242-254tests/shared-e2e/src/held-handles-scenario.ts:54(both apps)src/router/intercept-resolution.ts:320is the only caller; cache hit goesmatch-middleware/intercept-resolution.ts:155→resolveInterceptLoadersOnlyskills/caching/SKILL.md:192docs/design/prefetch-false.md:1041-1073;skills/view-transitions/SKILL.md:468-487;docs/internal/suspense-contract.mdrule 5 and "What the audit does not see"revalidate(true)/revalidate(false)) is not on maingh pr view 1130: open draft. The skill does not mention the shorthand.Code-change summary
packages/rangojs-router/skills/ux-architecture/SKILL.md(new).packages/rangojs-router/skills/rango/SKILL.md: one index row under "Structure & routing", sized to the existing column so the table does not reflow.packages/rangojs-router/skills/catalog.json: regenerated (node tools/generate-skills-catalog.mjs).packages/rangojs-router/docs/README.md: a one-sentence pointer.tools/check-docs-api.mjs: the new skill joins the guarded docs, so a renamed API it names fails CI.Test plan
node tools/check-skills.mjs: OK, 48 skills.node tools/check-docs-api.mjs: 321 identifiers verified (the new skill's 39 included,--list).node tools/check-doc-links.mjs: OK.node tools/generate-skills-catalog.mjs --check: current, rango table complete both ways.oxfmt --check .: clean.oxlint --deny-warnings: 0 warnings, 0 errors.serveShellRequestprobe for the revalidation counts above (9 cases, passed).test:unit:all: not run; no TypeScript source changed./simplifyand an Opus review of the diff.Notes
skills/app-structure/(dc-t41) is on another branch, andcheck-skillsfails a backticked/app-structurethat does not resolve. After both branches merge, turn the three "app-structure skill" mentions (Not this skill if, 2.2, section 6) into`/app-structure`.CHANGELOG.mdfor the fix" rather than naming 0.24.3. When the fix lands, the recipe 3.1 note and the 2.3 table cell can move the cart loader back to the grouplayout()if that is preferred; the server-layout placement stays correct either way.prefetch: falseand per-loaderrevalidate()(feat(router): intercept() loaders are loader segments of their slot: prefetch: false, per-loader revalidate() and handles reach modals #1132) are inCHANGELOG.md"Unreleased"; the skill describes them as current, which holds from the next release.Update 2026-10-13
clientUrls()layout()loader is now held on navigation, as on a server layout. The defaults tables and recipes say so, the known gap is removed, and therevalidate(true|false)shorthand (feat(router): revalidate(true) and revalidate(false) shorthand in urls() and clientUrls() #1130) is used.ctx.reverse(".x"),useReverse(routes)with the module's generated.gen.ts). The module never knows its mount, and mount params (/:accountId/:projectId,/:locale,/:locale?) are filled automatically.useReverse(routes), or the layout gets arevalidate()that compares the param.throw rewrite()from middleware (same URL, no round trip), and its limits.