UNPKG

shelving

Version:

Toolkit for using data in JavaScript.

187 lines (139 loc) 6.98 kB
# Router Client-side routing for shelving apps. Two pieces: | Component | Job | | --------------- | ---------------------------------------------------------------------------------------------- | | `<Navigation>` | One per app. Owns URL state, intercepts link clicks, listens for `popstate`. | | `<Router>` | Pure matcher. Reads URL from `<Meta>`, matches `routes`, renders the matched element. | Plus `requireNavigation()` for imperative `forward()` / `redirect()` calls from anywhere in the tree. `<Router>` reads URL state from `<Meta>`, so it works with no `<Navigation>` at all (SSR, static rendering, tests). `<Navigation>` is what publishes a *live* URL into `<Meta>` on the client. ## Basic setup ```tsx <HTML url={initialUrl} root="https://example.com/"> <Navigation> <Router routes={{ "/": HomePage, "/users/{id}": UserPage, "/about": AboutPage, }}/> </Navigation> </HTML> ``` - Route keys are `AbsolutePath` strings starting with `/`. - Placeholders (`{id}`, `:id`, `[id]`, `${id}`, `{{id}}`) are passed to function/component routes as props (merged with URL `?query` params; placeholders win on conflict). - A string value (e.g. `"/users/123"`) is a redirect — visiting the key path navigates to the target. - `<Router>` itself accepts `PossibleMeta` props (`url`, `base`, etc.) to override the surrounding context. ## Route value types | Value | Behaviour | | --------------------- | ---------------------------------------------------------------------- | | `RouteComponent` | Rendered as `<Component {...params}/>` with merged placeholder + query. | | `AbsolutePath` string | Redirects to that path (placeholders resolved against the source). | | `ReactElement` | Rendered as-is — use for layout wrapping or composing inner routers. | ## Placeholder syntax All forms produce the same matched value — pick whichever reads best: | Form | Single segment | Catchall (one+ segments, also matches empty) | | ----------------- | ---------------------- | -------------------------------------------- | | Anonymous | `*` (named `"0"`) | `**` / `***` / `****` (named `"0"`) | | Colon | `:name` | `:name*` / `:name**` | | Single brace | `{name}` | `{...name}` / `{name*}` / `{....name}` | | Square bracket | `[name]` | `[...name]` / `[name*]` | | Dollar brace | `${name}` | `${...name}` / `${name*}` | | Double brace | `{{name}}` | `{{...name}}` / `{{name*}}` | Modifier chars are tolerant: one-or-more stars and three-or-more dots are all equivalent. So `{path*}`, `{path**}`, `{...path}`, and `{....path}` all behave the same. Catchall placeholders allow empty values, so a trailing catchall matches the trailing-slash-absent variant too — `/files/{...path}` matches both `/files`, `/files/`, and `/files/a/b/c`. ## Layout wrapping Put layout JSX as the route value with another `<Router>` inside. ```tsx const SIDEBARRED_ROUTES = { "/users": <UsersPage/>, "/users/{id}": <UserPage/>, "/settings": <SettingsPage/>, }; <Router routes={{ "/": <HomePage/>, "/{...path}": ( <SidebarLayout> <Router routes={SIDEBARRED_ROUTES}/> </SidebarLayout> ), }}/> ``` The outer router matches everything, wraps in `<SidebarLayout>`, and hands off to the inner router. Since `<Router>` reads its URL from `<Meta>`, the inner router sees the same URL. ## Section / microsite pattern A self-contained "section" of the app — its own URL prefix, its own routes — composes via a catchall + a function route value that hands the captured sub-path to a nested router. ```tsx <Router routes={{ "/": <HomePage/>, "/users/{...path}": ({ path = "/" }) => ( <Router routes={{ "/": UsersPage, "/{id}": UserPage, }} url={path}/> ), }}/> ``` The outer router captures everything under `/users` into the `path` placeholder, then the inner router treats `path` as its starting URL. The inner router's `"/"` matches the bare `/users`; `/{id}` matches `/users/123`. Pull that out into a dedicated component for readability: ```tsx const USER_ROUTES = { "/": UsersPage, "/{id}": UserPage, "/{id}/edit": UserEditPage, }; export function UserRouter({ path = "/" }: { path?: AbsolutePath }) { return <Router routes={USER_ROUTES} url={path}/>; } // then at the call site: <Router routes={{ "/": <HomePage/>, "/users/{...path}": ({ path }) => <UserRouter path={path}/>, "/blog/{...path}": ({ path }) => <BlogRouter path={path}/>, }}/> ``` Each section module owns its routes and exposes one component. The top-level router stays a flat list of section prefixes. ## Stacking layouts and sections The two patterns compose. Wrap a bunch of routes in a layout, then route further inside it: ```tsx const SIDEBARRED_ROUTES = { "/": <Dashboard/>, "/users/{...path}": ({ path }) => <UserRouter path={path}/>, "/blog/{...path}": ({ path }) => <BlogRouter path={path}/>, "/settings": <SettingsPage/>, }; <Router routes={{ "/login": <LoginPage/>, // no sidebar "/{...path}": ( // everything else wrapped <SidebarLayout> <Router routes={SIDEBARRED_ROUTES}/> </SidebarLayout> ), }}/> ``` `/login` skips the sidebar. Every other path goes through `<SidebarLayout>` and then the inner router decides what to render — including handing off to section routers via the catchall pattern. ## Navigation Inside a component, get the navigation store for imperative URL changes: ```tsx const nav = requireNavigation(); nav.forward("/users/123"); // push history nav.redirect("/login"); // replace history ``` Same-origin anchor clicks are intercepted automatically and turned into `forward()` calls. Add a `download` attribute to opt out. ## `<NavigationIsolate>` Force a full remount of children whenever the URL changes: ```tsx <NavigationIsolate> <ExpensiveStatefulThing/> </NavigationIsolate> ``` ## SSR / static rendering `<Router>` has no client requirements — it reads from `<Meta>` and re-renders when context changes. For static rendering, set `url` and `root` on the outer wrapper and skip `<Navigation>`: ```tsx renderToString( <HTML url={path} root="https://example.com/"> <Router routes={…}/> </HTML> ); ``` For client-side SPAs, wrap the same tree in `<Navigation>` for live URL updates. ## Base paths `root="https://example.com/app/"` is supported. The base path prefix is stripped from the URL before route matching via `matchURLPrefix`. URLs that fall outside the base render as `null` from the router.