{"id":23429836,"url":"https://github.com/axtk/routescape","last_synced_at":"2025-04-12T21:34:13.454Z","repository":{"id":268767150,"uuid":"905334048","full_name":"axtk/routescape","owner":"axtk","description":"Minimalist router for React apps","archived":false,"fork":false,"pushed_at":"2024-12-26T14:53:37.000Z","size":639,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-07T01:04:01.555Z","etag":null,"topics":["history-api","react","react-router","router","spa"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/routescape","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/axtk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-18T16:04:21.000Z","updated_at":"2024-12-26T14:53:40.000Z","dependencies_parsed_at":"2024-12-18T20:18:05.761Z","dependency_job_id":"e6a040f7-0700-4bc0-acfd-ec96505c775b","html_url":"https://github.com/axtk/routescape","commit_stats":null,"previous_names":["axtk/routescape"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/axtk%2Froutescape","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/axtk%2Froutescape/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/axtk%2Froutescape/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/axtk%2Froutescape/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/axtk","download_url":"https://codeload.github.com/axtk/routescape/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248636972,"owners_count":21137527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["history-api","react","react-router","router","spa"],"created_at":"2024-12-23T08:12:57.233Z","updated_at":"2025-04-12T21:34:13.449Z","avatar_url":"https://github.com/axtk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# routescape\n\nMinimalist router for React apps\n\n- Single way to match routes for components and prop values\n- Consistency with native APIs:\n    - route links are similar to HTML links\n    - route navigation interface is similar to `window.location`\n- Unopinionated route structure: routes are not necessarily hierarchical, collocated or otherwise tightly coupled\n- Middleware hook for actions ahead of route navigation\n- Utility hook to make link tags in static HTML content work like SPA route links\n- Compatibility with SSR\n\nInstallation: `npm i routescape`\n\n## `\u003cA\u003e`\n\nThe route link component `\u003cA\u003e` enabling SPA navigation has the same props as its HTML counterpart: the `\u003ca\u003e` tag. Apart from reducing some cognitive load, this allows to quickly migrate from plain HTML links to route links (or the other way around).\n\n```jsx\nimport {A} from 'routescape';\n\nlet Nav = () =\u003e (\n    \u003cnav\u003e\n        \u003cA href=\"/intro\"\u003eIntro\u003c/A\u003e\n    \u003c/nav\u003e\n);\n```\n\n### Navigation mode\n\nBy default, after the link navigation occurs, the user can navigate back by pressing the browser's *back* button. Optionally, by setting `data-navigation-mode=\"replace\"` a link component can be configured to replace the navigation history entry, which will prevent the user from returning to the previous location by clicking the browser's *back* button.\n\n## `\u003cArea\u003e`\n\n`\u003cArea\u003e`, the image map route link component, has the same props and semantics as its HTML counterpart: the `\u003carea\u003e` tag. Setting the optional `data-navigation-mode=\"replace\"` prop on `\u003cArea\u003e` has the same effect as with `\u003cA\u003e`.\n\n## `useRoute()`\n\n### Route matching\n\nThe functional route matching with the function returned from the `useRoute()` hook offers a simple and consistent way to render both components and prop values based on the current location.\n\n```jsx\nimport {A, useRoute} from 'routescape';\n\nlet App = () =\u003e {\n    let [route, withRoute] = useRoute();\n\n    return (\n        \u003c\u003e\n            \u003cnav\u003e\n                \u003cA href=\"/intro\" className={withRoute('/intro', 'active')}\u003e\n                    Intro\n                \u003c/A\u003e\n            \u003c/nav\u003e\n            {withRoute('/intro', (\n                \u003cmain\u003e\n                    \u003ch1\u003eIntro\u003c/h1\u003e\n                \u003c/main\u003e\n            ))}\n        \u003c/\u003e\n    );\n};\n```\n\nNote that both the intro link's `className` and `\u003cmain\u003e` are rendered in a similar fashion using the same route-matching function. `withRoute('/intro', x)` returns `x` only if the current location is `/intro`.\n\n(With the component-based route matching adopted by some routers, conditionally rendering a component and marking a link as active via its props have to be handled differently.)\n\n### Route matching fallback\n\nSimilarly to the ternary operator `condition ? x : y` (often seen with the general [conditional rendering](https://react.dev/learn/conditional-rendering) pattern), `withRoute()` accepts a fallback value as the optional third parameter: `withRoute(routePattern, x, y)`.\n\n```jsx\nimport {A, useRoute} from 'routescape';\n\nlet Nav = () =\u003e {\n    let [, withRoute] = useRoute();\n\n    return (\n        \u003cnav\u003e\n            \u003cA\n                href=\"/intro\"\n                className={withRoute('/intro', 'active', 'inactive')}\n            \u003e\n                Intro\n            \u003c/A\u003e\n        \u003c/nav\u003e\n    );\n};\n```\n\nIn the example above, the link is marked as `active` if the current location is `/intro`, and `inactive` otherwise.\n\nWith the third parameter omitted, `withRoute('/intro', 'active')` results in `undefined` with locations other than `/intro` (since the missing fallback parameter is effectively `undefined`), which is perfectly fine as well.\n\nAnother option would be to render a non-interactive `\u003cspan\u003e` for the active route, and a route link pointing to that route otherwise:\n\n```jsx\nimport {A, useRoute} from 'routescape';\n\nlet Nav = () =\u003e {\n    let [, withRoute] = useRoute();\n\n    return (\n        \u003cnav\u003e\n            {withRoute(\n                '/intro',\n                \u003cspan\u003eIntro\u003c/span\u003e,\n                \u003cA href=\"/intro\"\u003eIntro\u003cA\u003e,\n            )}\n        \u003c/nav\u003e\n    );\n};\n```\n\n### Route parameters\n\n`withRoute()` accepts route patterns of various types: `string | RegExp | (string | RegExp)[]`. The parameters of a regular expression route pattern (or of the first match in the array) are passed to the second and the third parameter of `withRoute()` if they are functions.\n\n```jsx\nlet App = () =\u003e {\n    let [, withRoute] = useRoute();\n\n    return (\n        \u003c\u003e\n            \u003cnav\u003e\n                \u003cA href=\"/intro\"\u003eIntro\u003c/A\u003e\n            \u003c/nav\u003e\n            {withRoute(/^\\/section\\/(?\u003cid\u003e\\d+)\\/?$/, ({id}) =\u003e (\n                \u003cmain\u003e\n                    \u003ch1\u003eSection #{id}\u003c/h1\u003e\n                \u003c/main\u003e\n            ))}\n        \u003c/\u003e\n    );\n};\n```\n\n### Unknown routes\n\nThe fallback parameter of `withRoute()` is also a way to handle unknown routes:\n\n```jsx\nconst routeMap = {\n    intro: '/intro',\n    sections: /^\\/section\\/(?\u003cid\u003e\\d+)\\/?$/,\n};\n\nconst knownRoutes = Object.values(routeMap);\n\nlet App = () =\u003e {\n    let [, withRoute] = useRoute();\n\n    return (\n        \u003c\u003e\n            \u003cnav\u003e\n                \u003cA href={routeMap.intro}\u003eIntro\u003c/A\u003e\n            \u003c/nav\u003e\n            {withRoute(routeMap.intro, (\n                \u003cmain\u003e\n                    \u003ch1\u003eIntro\u003c/h1\u003e\n                \u003c/main\u003e\n            ))}\n            {withRoute(routeMap.sections, ({id}) =\u003e (\n                \u003cmain\u003e\n                    \u003ch1\u003eSection #{id}\u003c/h1\u003e\n                \u003c/main\u003e\n            ))}\n            {withRoute(knownRoutes, null, (\n                \u003cmain className=\"error\"\u003e\n                    \u003ch1\u003e404 Not found\u003c/h1\u003e\n                \u003c/main\u003e\n            ))}\n        \u003c/\u003e\n    );\n};\n```\n\nNote that the last `withRoute()` results in `null` (that is no content) for all known routes and renders the error content for the rest unknown routes.\n\nAlthough the routes are grouped together in the example above, that's not a requirement. `withRoute()` calls are not coupled together, they can be split across separate components and files and arranged in any order (like any other conditionally rendered components).\n\n### Imperative route navigation\n\nTo jump to another route programmatically, there's the `route` object returned from the `useRoute()` hook:\n\n```jsx\nlet ProfileButton = ({signedIn}) =\u003e {\n    let [route] = useRoute();\n\n    let handleClick = () =\u003e {\n        route.assign(signedIn ? '/profile' : '/login');\n    };\n\n    return \u003cbutton onClick={handleClick}\u003eProfile\u003c/button\u003e;\n};\n```\n\nThis particular example is somewhat contrived since it could have been composed in a declarative fashion using the route link component `\u003cA\u003e`. Still, it demonstrates how the `route` object can be used in use cases where the imperative navigation is the only reasonable way to go.\n\nThe interface of the `route` object consists of the following parts:\n\n- SPA navigation via the History API:\n    - `.assign()`, `.replace()`, `.reload()`, and readonly properties: `.href`, `.pathname`, `.search`, `.hash`, semantically similar to `window.location`;\n    - `.back()`, `.forward()`, `.go(delta)`, corresponding to the [`history` methods](https://developer.mozilla.org/en-US/docs/Web/API/History#instance_methods);\n- route matching:\n    - `.matches(value)`, checking whether the current location matches the given `value`;\n    - `.match(value)`, returning matched parameters if the given `value` is a regular expression and `null` if the current location doesn't match the `value`.\n\n## `useNavigationStart()`\n\nThe `useNavigationStart()` hook allows to define routing *middleware*, that is intermediate actions to be done before the route navigation occurs.\n\n### Preventing navigation\n\nCommon use cases for preventing navigation are: warning about unsaved data before leaving the page or opening a preview widget for certain links instead of jumping to a new full-screen page.\n\nNavigation to another route can be prevented by returning `false` under certain conditions within the hook callback:\n\n```jsx\nimport {useNavigationStart} from 'routescape';\n\nlet App = () =\u003e {\n    let [hasUnsavedChanges, setUnsavedChanges] = useState(false);\n\n    useNavigationStart(() =\u003e {\n        if (hasUnsavedChanges)\n            return false;\n    }, [hasUnsavedChanges]);\n\n    return (\n        // app content\n    );\n};\n```\n\nIn this example, all route navigation is interrupted as long as `hasUnsavedChanges` is `true`.\n\n### Redirection\n\nRedirection to another route can be done by calling `route.assign()` within the hook callback:\n\n```jsx\nimport {useNavigationStart} from 'routescape';\n\nlet App = () =\u003e {\n    useNavigationStart(nextHref =\u003e {\n        if (nextHref === '/intro') {\n            route.assign('/');\n            return false;\n        }\n    }, [route]);\n\n    return (\n        // app content\n    );\n};\n```\n\nNote that the hook callback returns `false` when `nextHref` is `'/intro'`. This prevents the navigation to `/intro`.\n\nThe callback might as well contain additional checks before allowing the redirection (like whether the user has access to the target location).\n\n## `useNavigationComplete()`\n\nThe callback of the `useNavigationComplete()` hook is called after going through all routing middleware registered with the `useNavigationStart()` hook and after assigning the next route.\n\nThe `useNavigationComplete()` callback is first called when the component gets mounted if the route is already in the navigation-complete state.\n\n```jsx\nimport {useNavigationComplete} from 'routescape';\n\nlet App = () =\u003e {\n    useNavigationComplete(href =\u003e {\n        if (href === '/intro')\n            document.title = 'Intro';\n    }, []);\n\n    return (\n        // app content\n    );\n};\n```\n\n## `useRouteLinks()`\n\nA chunk of static HTML content is an example where the route link component `\u003cA\u003e` can't be directly used but it still might be desirable to make plain HTML links in that content behave as SPA route links. The `useRouteLinks()` hook can be helpful here:\n\n```jsx\nimport {useRef} from 'react';\nimport {useRouteLinks} from 'routescape';\n\nlet Content = ({value}) =\u003e {\n    let containerRef = useRef(null);\n\n    useRouteLinks(containerRef, 'a');\n\n    return (\n        \u003cdiv ref={containerRef}\u003e\n            {value}\n        \u003c/div\u003e\n    );\n};\n```\n\nIn this example, the `useRouteLinks()` hook makes all links matching the selector `'a'` inside the container referenced by `containerRef` act as SPA route links.\n\n## `\u003cRouter\u003e`\n\nServer-side rendering and unit tests are the examples of the environments lacking a global location (such as `window.location`). They are the prime use cases for the location provider, `\u003cRouter\u003e`.\n\nLet's consider an *express* application route as an example:\n\n```jsx\nimport {renderToString} from 'react-dom/server';\nimport {Router} from 'routescape';\n\napp.get('/', (req, res) =\u003e {\n    let html = renderToString(\n        \u003cRouter location={req.originalUrl}\u003e\n            \u003cApp/\u003e\n        \u003c/Router\u003e,\n    );\n\n    res.send(html);\n});\n```\n\nThe value passed to the router's `location` prop can be accessed via the `useRoute()` hook:\n\n```jsx\nlet [route, withRoute] = useRoute();\n\nconsole.log(route.href); // returns the router's `location`\n```\n\nBoth `route` and `withRoute()` returned from `useRoute()` operate based on the router's `location`.\n\n`\u003cRouter\u003e` can be used with client-side rendering as well. In most cases, it is unnecessary since by default the route context takes the global location from `window.location` if it's available.\n\n### Custom routing\n\nThe location provider component `\u003cRouter\u003e` can be used to redefine the route matching behavior.\n\n```jsx\nimport {Route, getPath, Router} from 'routescape';\n\nexport class PathRoute extends Route {\n    getHref(location) {\n        // disregard `search` and `hash`\n        return getPath(location, {search: false, hash: false});\n    }\n}\n\nlet App = () =\u003e (\n    \u003cRouter location={new PathRoute(url)}\u003e\n        \u003cAppContent/\u003e\n    \u003c/Router\u003e\n);\n```\n\nBy default, routing relies on the entire URL. In this example, we've redefined this behavior to disregard the `search` and `hash` portions of the URL.\n\nExtending the `Route` class gives plenty of room for customization. This approach allows in fact to go beyond the URL-based routing altogether.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faxtk%2Froutescape","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faxtk%2Froutescape","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faxtk%2Froutescape/lists"}