https://github.com/axtk/routescape
Minimalist router for React apps
https://github.com/axtk/routescape
history-api react react-router router spa
Last synced: about 1 year ago
JSON representation
Minimalist router for React apps
- Host: GitHub
- URL: https://github.com/axtk/routescape
- Owner: axtk
- Created: 2024-12-18T16:04:21.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-12-26T14:53:37.000Z (over 1 year ago)
- Last Synced: 2025-04-07T01:04:01.555Z (about 1 year ago)
- Topics: history-api, react, react-router, router, spa
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/routescape
- Size: 624 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# routescape
Minimalist router for React apps
- Single way to match routes for components and prop values
- Consistency with native APIs:
- route links are similar to HTML links
- route navigation interface is similar to `window.location`
- Unopinionated route structure: routes are not necessarily hierarchical, collocated or otherwise tightly coupled
- Middleware hook for actions ahead of route navigation
- Utility hook to make link tags in static HTML content work like SPA route links
- Compatibility with SSR
Installation: `npm i routescape`
## ``
The route link component `` enabling SPA navigation has the same props as its HTML counterpart: the `` tag. Apart from reducing some cognitive load, this allows to quickly migrate from plain HTML links to route links (or the other way around).
```jsx
import {A} from 'routescape';
let Nav = () => (
Intro
);
```
### Navigation mode
By 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.
## ``
``, the image map route link component, has the same props and semantics as its HTML counterpart: the `` tag. Setting the optional `data-navigation-mode="replace"` prop on `` has the same effect as with ``.
## `useRoute()`
### Route matching
The 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.
```jsx
import {A, useRoute} from 'routescape';
let App = () => {
let [route, withRoute] = useRoute();
return (
<>
Intro
{withRoute('/intro', (
Intro
))}
>
);
};
```
Note that both the intro link's `className` and `` are rendered in a similar fashion using the same route-matching function. `withRoute('/intro', x)` returns `x` only if the current location is `/intro`.
(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.)
### Route matching fallback
Similarly 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)`.
```jsx
import {A, useRoute} from 'routescape';
let Nav = () => {
let [, withRoute] = useRoute();
return (
Intro
);
};
```
In the example above, the link is marked as `active` if the current location is `/intro`, and `inactive` otherwise.
With 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.
Another option would be to render a non-interactive `` for the active route, and a route link pointing to that route otherwise:
```jsx
import {A, useRoute} from 'routescape';
let Nav = () => {
let [, withRoute] = useRoute();
return (
{withRoute(
'/intro',
Intro,
Intro,
)}
);
};
```
### Route parameters
`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.
```jsx
let App = () => {
let [, withRoute] = useRoute();
return (
<>
Intro
{withRoute(/^\/section\/(?\d+)\/?$/, ({id}) => (
Section #{id}
))}
>
);
};
```
### Unknown routes
The fallback parameter of `withRoute()` is also a way to handle unknown routes:
```jsx
const routeMap = {
intro: '/intro',
sections: /^\/section\/(?\d+)\/?$/,
};
const knownRoutes = Object.values(routeMap);
let App = () => {
let [, withRoute] = useRoute();
return (
<>
Intro
{withRoute(routeMap.intro, (
Intro
))}
{withRoute(routeMap.sections, ({id}) => (
Section #{id}
))}
{withRoute(knownRoutes, null, (
404 Not found
))}
>
);
};
```
Note 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.
Although 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).
### Imperative route navigation
To jump to another route programmatically, there's the `route` object returned from the `useRoute()` hook:
```jsx
let ProfileButton = ({signedIn}) => {
let [route] = useRoute();
let handleClick = () => {
route.assign(signedIn ? '/profile' : '/login');
};
return Profile;
};
```
This particular example is somewhat contrived since it could have been composed in a declarative fashion using the route link component ``. Still, it demonstrates how the `route` object can be used in use cases where the imperative navigation is the only reasonable way to go.
The interface of the `route` object consists of the following parts:
- SPA navigation via the History API:
- `.assign()`, `.replace()`, `.reload()`, and readonly properties: `.href`, `.pathname`, `.search`, `.hash`, semantically similar to `window.location`;
- `.back()`, `.forward()`, `.go(delta)`, corresponding to the [`history` methods](https://developer.mozilla.org/en-US/docs/Web/API/History#instance_methods);
- route matching:
- `.matches(value)`, checking whether the current location matches the given `value`;
- `.match(value)`, returning matched parameters if the given `value` is a regular expression and `null` if the current location doesn't match the `value`.
## `useNavigationStart()`
The `useNavigationStart()` hook allows to define routing *middleware*, that is intermediate actions to be done before the route navigation occurs.
### Preventing navigation
Common 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.
Navigation to another route can be prevented by returning `false` under certain conditions within the hook callback:
```jsx
import {useNavigationStart} from 'routescape';
let App = () => {
let [hasUnsavedChanges, setUnsavedChanges] = useState(false);
useNavigationStart(() => {
if (hasUnsavedChanges)
return false;
}, [hasUnsavedChanges]);
return (
// app content
);
};
```
In this example, all route navigation is interrupted as long as `hasUnsavedChanges` is `true`.
### Redirection
Redirection to another route can be done by calling `route.assign()` within the hook callback:
```jsx
import {useNavigationStart} from 'routescape';
let App = () => {
useNavigationStart(nextHref => {
if (nextHref === '/intro') {
route.assign('/');
return false;
}
}, [route]);
return (
// app content
);
};
```
Note that the hook callback returns `false` when `nextHref` is `'/intro'`. This prevents the navigation to `/intro`.
The callback might as well contain additional checks before allowing the redirection (like whether the user has access to the target location).
## `useNavigationComplete()`
The callback of the `useNavigationComplete()` hook is called after going through all routing middleware registered with the `useNavigationStart()` hook and after assigning the next route.
The `useNavigationComplete()` callback is first called when the component gets mounted if the route is already in the navigation-complete state.
```jsx
import {useNavigationComplete} from 'routescape';
let App = () => {
useNavigationComplete(href => {
if (href === '/intro')
document.title = 'Intro';
}, []);
return (
// app content
);
};
```
## `useRouteLinks()`
A chunk of static HTML content is an example where the route link component `` 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:
```jsx
import {useRef} from 'react';
import {useRouteLinks} from 'routescape';
let Content = ({value}) => {
let containerRef = useRef(null);
useRouteLinks(containerRef, 'a');
return (
{value}
);
};
```
In this example, the `useRouteLinks()` hook makes all links matching the selector `'a'` inside the container referenced by `containerRef` act as SPA route links.
## ``
Server-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, ``.
Let's consider an *express* application route as an example:
```jsx
import {renderToString} from 'react-dom/server';
import {Router} from 'routescape';
app.get('/', (req, res) => {
let html = renderToString(
,
);
res.send(html);
});
```
The value passed to the router's `location` prop can be accessed via the `useRoute()` hook:
```jsx
let [route, withRoute] = useRoute();
console.log(route.href); // returns the router's `location`
```
Both `route` and `withRoute()` returned from `useRoute()` operate based on the router's `location`.
`` 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.
### Custom routing
The location provider component `` can be used to redefine the route matching behavior.
```jsx
import {Route, getPath, Router} from 'routescape';
export class PathRoute extends Route {
getHref(location) {
// disregard `search` and `hash`
return getPath(location, {search: false, hash: false});
}
}
let App = () => (
);
```
By 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.
Extending the `Route` class gives plenty of room for customization. This approach allows in fact to go beyond the URL-based routing altogether.