Claude Fable 5.1 & GPT-6 Astra packages are live

Routing

Free

Client and app routing — URL as state, nested layouts, guards that are not security, loading and error boundaries, and scroll and focus on navigation.

192 lines8.0 KB Grok Frontend
targetModels
Grok 4.6Grok 4.5Grok 4 FamilyGrok Code FastFuture Grok Models
name
routing
category
Frontend
description
Client and app routing — URL as state, nested layouts, guards that are not security, loading and error boundaries, and scroll and focus on navigation.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Grok: scripts/model-profiles.json -->

#Non-negotiable

The constraints hoisted below override anything later in this document. Read them first; the rest is rationale.


#Purpose

Rules for routing in a web application. The URL is the application's public interface: it is what users bookmark, share, and return to. Treat it as a contract, not an implementation detail.

Route-level code splitting is Frontend/code-splitting; metadata is Frontend/metadata.


#The URL carries state

Anything that changes what the user sees belongs in the URL.

bash
/orders?status=paid&sort=-createdAt&page=2      ✅ shareable, bookmarkable, back-button-correct
/orders                                          ❌ filters in component state
In the URLNot in the URL
Filters, sort, paginationWhether a dropdown is open
Selected record or tabHover state, focus
Search queryIn-progress form values (until submit)
View mode (list/grid)Transient toasts

If state is not in the URL, a refresh loses it and a shared link shows something different — reported by users as "the link doesn't work".

Rules for the URL itself:

  • Lowercase, hyphenated, plural collections: /payment-methods/{id}.
  • Stable. A changed URL is a broken bookmark and a lost search ranking; when you must change one, 301 the old path. → Frontend/seo
  • Never put a secret, token or personal data in a query string — it lands in server logs, browser history and Referer headers. → API/api-security
  • Omit defaults (?page=1 adds nothing) so the canonical URL is unambiguous.
  • Validate and coerce every parameter: a route parameter is untrusted input.

#Structure with nested layouts

ini
app/
  layout.tsx                 # shell: header, nav — never re-renders on child navigation
  orders/
    layout.tsx               # orders sidebar
    page.tsx                 # /orders
    [id]/page.tsx            # /orders/:id
    loading.tsx              # streamed fallback
    error.tsx                # boundary for this subtree

Nested layouts preserve state across navigation within a section — scroll position in a sidebar, an open panel — which a flat route table cannot do.

  • Colocate loading and error boundaries per segment, so a failure in one panel does not blank the page.
  • Every route needs a not-found path for a bad or deleted id, returning a real 404 rather than an empty page.
  • Keep dynamic segments shallow. /users/:u/orders/:o/items/:i exposes a hierarchy that will change; one level of nesting is usually enough. → API/rest

#Route guards are not authorization

tsx
// A client-side redirect. The data was already fetched, or is one fetch away.
if (!user.isAdmin) return <Navigate to="/" />;

A client-side guard is a user-experience feature: it avoids showing a page that will fail. It is not a control — the user can call the API directly, and the JavaScript that decides is running on their machine.

  • Enforce authorization on the server, on every request that returns data. → Backend/authorization
  • Redirect unauthenticated users to a login page that preserves the intended destination (?next=/orders/123), and validate that parameter against an allowlist of internal paths — an unvalidated redirect target is an open-redirect vulnerability used in phishing.
  • Never render protected content and hide it with CSS. It is in the DOM.

#Navigation must not lose the user

Client-side routing replaces a full page load, so the browser behaviours it provided must be reimplemented.

BehaviourRequirement
ScrollReset to top on a new route; restore position on back/forward
FocusMove to the main heading or <main> after navigation
AnnouncementScreen readers get no page-change event — announce it in a live region
TitleUpdate <title> per route
Pending stateShow progress for navigations over ~200ms
Unsaved changesBlock navigation and confirm → Frontend/forms

Focus and announcement are the ones most often missed, and they make an application unusable with a screen reader — the user activates a link and nothing tells them anything changed. → Testing/accessibility

Use real <a href> elements (or the router's <Link>, which renders one). A <div onClick={navigate}> breaks middle-click, open-in-new-tab, copy-link, and keyboard access.


#Data and transitions

  • Fetch per route with the router's loader or a server component, so the request starts with the navigation rather than after the component mounts.
  • Prefetch on hover, focus or viewport entry — the code and the data.
  • Keep the old view visible during a pending navigation (useTransition, startTransition) rather than flashing a spinner over content that was fine.
  • Handle a failed navigation: leave the user where they were with an error, not on a blank route.

#Anti-patterns

Anti-patternWhy it failsFix
Filters in component stateNot shareable; lost on refresh; back button brokenPut them in the URL
Secrets in query stringsLogged, in history, leaked via RefererNever
Defaults serialised into the URLMultiple URLs for one viewOmit defaults
Unvalidated route parametersUntrusted input reaching queriesParse and coerce
URLs changed without redirectsBroken bookmarks and rankings301 the old path
Client-side guard as authorizationRuns on the attacker's machineServer-side enforcement
Unvalidated ?next= redirectOpen redirect used for phishingAllowlist internal paths
Protected content hidden with CSSPresent in the DOMDo not send it
<div onClick={navigate}>Breaks middle-click, new tab, keyboard<a href> / <Link>
No scroll restorationBack button loses the user's placeRestore on pop navigation
No focus managementScreen reader users get no feedbackFocus <main> and announce
No route-level error boundaryOne failure blanks the pageBoundary per segment
Missing not-found handlingDeleted records render an empty pageReal 404 route
Fetching after mountWaterfall: navigate, render, then fetchRoute loaders
Spinner over good contentFlashing during fast navigationsTransitions
Deep dynamic nestingFreezes a hierarchy that will changeFlatten

#Checklist

  • Verify: Filters, sort, pagination, tab and selection are all in the URL
  • Verify: No secrets or personal data appear in URLs
  • Verify: Default parameter values are omitted
  • Verify: Every route parameter is validated and coerced
  • Verify: URL changes are accompanied by permanent redirects
  • Verify: Layouts are nested so shared shell state survives navigation
  • Verify: Each segment has loading and error boundaries
  • Verify: A not-found route exists and returns a real 404
  • Verify: Authorization is enforced server-side; client guards are UX only
  • Verify: Post-login redirect targets are validated against an allowlist
  • Verify: Navigation uses real anchor elements
  • Verify: Scroll resets on new routes and restores on back/forward
  • Verify: Focus moves to the main content after navigation
  • Verify: Route changes are announced to assistive technology
  • Verify: The document title updates per route
  • Verify: Data loads with the navigation, not after mount
  • Verify: Routes and their data are prefetched on intent
  • Verify: Pending navigations keep the previous view visible