npm package: @jreusch/router-preact
Description: Preact bindings for @jreusch/router
Author: Joshua Reusch
Latest version: 1.0.5

Preact library for [@jreusch/router](https://www.npmjs.com/package/@jreusch/router), which only exists because I was unsatisfied with existing solutions for Preact. I hope you like this one, as well!

# Features

* 🚀 **Tiny** - only 200LOC of Typescript, ~2k minified+gziped in production
* 🤘 **Well-tested** - Lots of tests for the core functionality
* 🤩 **Powerful** supports custom patterns, backtracking, different router backends, automatic props, async routes, and more!


# Quick Start

Install the library, you do not need to instead `@jreusch/router` separately:

```bash
npm install @jreusch/router-preact
```

Choose `<HashRouter>` or `<PathRouter>`, and wrap your pages inside of the `<Route>` component:

```tsx
import { PathRouter, Route } from '@jreusch/router-preact'
import { h } from 'preact'

export default function App() {
    return (
        <PathRouter>
            <Route pattern="/">
                <Index />
            </Route>

            {/* providing a single component will forward params as props */}

            <Route
                pattern="/blog/:postId"
                children={Blog}
            />

            <Route pattern="/about">
                <About />
            </Route>

            <Route pattern="/*">
                <NotFound404 />
            </Route>
        </PathRouter>
    )
}
```

Use `<Link>` instead of `<a>` tags to enable client-side navigation and active/inactive states:

```ts
import { Link } from '@jreusch/router-preact'
import { h } from 'preact'

export default function Navbar() {
    const links = [
        { to: '/', label: 'Home' },
        { to: '/blog', label: 'Blog' },
        { to: '/about', label: 'About us' }
    ]

    return (
        <nav>
            <ul>
                {links.map(({ to, label }) => (
                    <li>
                        <Link
                            href={to}
                            className="navbar-link"
                            activeClassName="active"
                        >
                            {label}
                        </Link>
                    </li>
                ))}
            </ul>
        </nav>
    )
}
```

Read the rest of the documentation to see what else is there 🙂

You might also want to [learn more about the pattern syntax](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#in-depth-syntax-and-semantics) in the core API documentation.

# Components

## &lt;PathRouter&gt;, &lt;HashRouter&gt;, &lt;MemoryRouter&gt;, &lt;PathWithBaseRouter&gt;

Wrap the root of your application inside of a router component. The router provides the context, which all other hooks and components depend upon, and defines global configuration, like where to get the URL from, and what happens if you navigate somewhere else. A router makes sure only a single route is rendered at a time.

### Routers

| Name                 | Description
|----------------------|-------------
| `PathRouter`         | The default choice for modern SPAs. Uses the path and provides client-side navigation, but requires some server configuration to work properly.
| `PathWithBaseRouter` | Similar to `PathRouter`, but also allows you to specify a `base` URL.
| `HashRouter`         | If changing the server configuration is not possible, you can use the HashRouter to instead use hash URLs.
| `MemoryRouter`       | A router that works fully in-memory and can be used for server-side rendering.

Router components correspond to the `createXYZRouter()` functions in the [core library](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#client-side-routers), so you can also go there to learn more about their differences!

The different flavors of routers change which internal [router object](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#client-side-routers) is used, but behave exactly the same otherwise. They are in thin convenience wrappers around the `<Router>` component, which allows you to [control the router dynamically](#dynamic-routers).


### Props

| Name       | Type                                       | Description
|------------|--------------------------------------------|-------------
| `children` | `jsx`                                      | Childreen rendered inside the router context, for example `<Route>` or `<AsyncRoute>`.
| `onChange` | `(newUrl: string, oldUrl: string) => void` | Optional callback invoked whenever the URL changes
| `base`     | `string`                                   | Base URL to strip from the path. (`<PathWithBaseRouter>` only)

See also [Dynamic Routers](#dynamic-routers) if you need to switch between different Router components depending on the context.

## &lt;Route&gt;

The `<Route>` component wraps your page, only rendering its children if the URL matches. It needs to be placed inside of a `*Router` component to access the context. This will make sure there will only be a single active `<Route>` per router. Routes are checked in-order, so if you have multiple routes matching the same URL, only the first one will be active and rendered.

 You can pass a single component constructor or function to `children` to automatically pass the matched params as props to that component. Otherwise, [the useParams hook](#useparams-paramsnull) provides a way to access the params inside of the children of the `<Route>`. If you instead want to asynchronously load your pages, check out the [AsyncRoute component](#ltasyncroutegt) instead!

```tsx
(
    {/* directly specify contents */}
    <Route pattern="/">
        <h1>Welcome to my site</h1>
        <p>
            I'm so happy to see you!
        </p>
    </Route>

    {/* This passes the `postId` prop to te BlogPost component. */}
    <Route
        pattern="/blog/:postId"
        children={BlogPost}
    />

    {/* Routes are checked in the same order they are rendered,
        so specifying a catch-all last provides a way to have a 404 */}
    <Route pattern="/*">
        <NotFound404 />
    </Route>
)
```

### Props

| Name        | Type     | Description
|-------------|----------|-------------
| `pattern`   | `string` | Render this route whenever this pattern matches
| `children`  | `jsx`    | JSX or component to render whenver the pattern matches. If a component is given, matched params will be passed as props.


## &lt;AsyncRoute&gt;

An `<AsyncRoute>` is like a `<Route>`, but loads the page component using a Promise, making it ideal to use bundle-splitting in your app. It always expects a `default` property on the resolved object, so you can directly provide a function calling `import(...)`. The function returning the promise is called with the params, which means it will be called every time the params change. You can for example use dynamic imports  ([vitejs](https://vitejs.dev/guide/features.html#dynamic-import)) to load different components depending on the params. It is assumed that it has its own internal caching mechanism (like the `import(...)` function does by default).



```tsx
(
    <AsyncRoute
        pattern="/blog/:postId"
        component={() => import('./pages/BlogPost.tsx')}
    />

    {/* params are passed to the loader function, so you can for example
         use dynamic imports to load different components depending on the params */}
    <AsyncRoute
        pattern="/static/:name"
        component={({ name }) => import(`./static/${name}.tsx`)}
    />

    {/* `loading` will be shown while the Promise is resolving */}
    <AsyncRoute
        pattern="/admin/*"
        component={() => import('./pages/AdminApp.tsx')}
        loading={(
            <div>
                Loading backend...
            </div>
        )}
    />
)
```

### Props

| Name        | Type                | Description
|-------------|---------------------|-------------
| `pattern`   | `string`            | Render this route whenever this pattern matches
| `component` | `Params => Promise` | Loading function for the component. Should resolve with default-exported component. Matched params will be passed as props.
| `loading`   | `jsx?`              | Optional component to render while loading, instead. If a component is given, matched params will be passed as props.


## &lt;Redirect&gt;

A render-less component that redirects to a new location as soon as a pattern matches. Variables used in the `from` pattern can be re-used in the `to` pattern to make dynamic redirects.

By default, `<Redirect>` will replace the current URL, but you can also explicitely set `replace={false}` to disable this behaviour.

A redirect will check if there is another `<Route>` that matches the URL, and will only redirect if it would be the active route, i.e. the one being rendered.

```tsx
(
    // static redirect, URL has to be exactly /rambling to redirect to /blog
    <Redirect from="/ramblings" to="/blog" />

    // redirect /about to /about-us, pushing a new url
    <Redirect from="/about" to="/about-us" replace={false} />

    // Use variables to redirect a prefix:
    // e.g. /posts/hello-sailor will be redirected to /blog/hello-sailor
    <Redirect from="/posts/:slug*" to="/blog/:slug*" />
)
```

### Props

| Name      | Type       | Description
|-----------|------------|-------------
| `from`    | `string`   | Redirect whenever current URL matches this pattern
| `to`      | `string`   | Redirect to this other pattern, stringified with the params returned by the match
| `replace` | `boolean?` | If `false`, navigate to the new URL; replace the current URL otherwise.

## &lt;Link&gt;

Use `<Link>` whenever you want an internal link with client-side navigation enabled. Links enable different classes based on whether or not the link's URL matches the current URL, making them perfect for nav bars. By default, a link is considered "active" whenever the current URL starts with the url in the link.

```tsx
(
    <Link href="/" className="logo"><Logo /></Link>

    // set .active class whenever the URL starts with /blog
    <Link
        href="/blog"
        className="nav-link"
        activeClassName="active"
    >
        Blog
    </Link>

    // set .active class only when the URL is exactly /about-us,
    // but not on /about-us/contact
    <Link
        href="/about-us"
        className="nav-link"
        activeClassName="active"
        exact={true}
    >
        About us
    </Link>

    // you can use patterns and params to build urls, too:
    <Link href="/blog/:postId" params={{ postId: 1234 }}>
        What I've been up to
    </Link>
)
```

If you need something more custom, check out the [useMatch](#usematchpattern-string-allowpartial-boolean-paramsnull) hook instead!

### Props

| Name                | Type      | Description
|---------------------|-----------|-------------
| `href`              | `string`   | Internal URL or pattern to navigate to when clicked
| `params`            | `Params?`  | If set, use these params to stringify the given pattern
| `className`         | `string?`  | Class that is always set
| `activeClassName`   | `string?`  | Class that is set if the current URL matches the given pattern
| `inactiveClassName` | `string?`  | Class that is set if the current URL does not match the given pattern
| `exact`             | `boolean?` | If `true`, the link is considered active if the given pattern matches exactly. Otherwise, a the given pattern has to only match the beginning of the current URL.


# Hooks

## useCurrentUrl(): string|null

Get the current URL that the router uses, also subscribing to route changes. The component this hook is used in will re-render whenever the URL changes.

Because the current router might not match the URL at all (for example when using a `<PathWithBaseRouter>`), this function might return `null` to indicate that.

## useMatch(pattern: string, allowPartial?: boolean): Params|null

Provided a pattern (as a string), watches the current URL and matches it against that pattern, returning the variables if it matches, or `null` if it doesn't. Please keep in mind that the component this hook is used in will re-render on every URL change, even if the matched params (if any) don't change.

This hook can for example be used to build custom `<Link>` components.

Setting the `allowPartial` flag, the pattern does not need to match the entire URL, but will early-out as soon as a portion of the URL matches.

## useRouter(): Router

Get access to the underlying `@jreusch/router` router object, allowing programmatic navigation:

```js
const router = useRouter()

router.navigate('/somewhere-else') // go to a new url
router.go(-1) // go back
router.go(1) // go forward
```

You can look at the [router documentation](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#router-api) to learn about all the methods available!

## useParams(): Params|null

A hook that can be used inside of a `<Route>`, where it will provide the parsed params, without needing to pass it down. It does **not** allow to access the parameters _outside_ of the `<Route>`.

# Advanced

I wrote [a guide](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#in-depth-syntax-and-semantics) on how patterns, segments and matching works. I highly recommend to check it out!

The entire API of [@jreusch/router](https://www.npmjs.com/package/@jreusch/router) is also exported in this package, so if you want to `parse` and `match` manually, you can just import them directly.

## Dynamic Routers

`<PathRouter>`, `<HashRouter>`, etc. provide simple and convenient components for when you just want to build a simple client-side app. But what if you need to support SSR from the same codebase as well? What if you're building a widget, and other people should be able to control how routing works?

The more low-level `<Router>` component allows you to provide a [@jreusch/router router object](https://gitlab.com/arkandos/ts-router/-/blob/master/packages/core/README.md#router-api) as a prop, making it possible to dynamically switch between them:

```tsx
import { createPathRouter, createMemoryRouter, Router } from '@jreusch/router-preact'
import { h } from 'preact'
import { useState } from 'preact/hooks'

export default function App() {
    // use a memory router if SSR, and a path router otherwise.
    const [router] = useState(() => {
        if (isSSR()) {
            return createMemoryRouter()
        } else {
            return createPathRouter()
        }
    })

    return (
        <Router router={router}>
            {/* ... */}
        </Router>
    )
}
```

The underlying router can be swapped at runtime, updating all subscriptions automatically.

## Route order and the rendering process

The rendering process of this library is unbelievably simple: On every URL change, all `<Route>` components re-render, and the first matching one sets some global state to let the others know that a match has been found. There is no path-rank, no special data structure, or crazy component interactions to register/unregister routes.

While some of those techniques might make routing more predictable and/or faster, I believe that most SPAs don't have hundreds or thausands of individual routes, where complex data structures would outperform a simple array.

One caveat of this is that it depends on the order the `<Route>` components where first rendered, so it's almost always easiest to just keep them as direct children to the `<Router>`, making the order obvious. In general, it is best to not have overlapping routes at all, except for a single "catch-all" route at the end.


## Focus handling

Preact unfortunately does not support `autoFocus={true}` the same way React does (see [this](https://github.com/preactjs/preact/issues/1255) and [that](https://github.com/preactjs/preact/issues/2328)). To give focus to an element after navigating to that page, use effects and refs instead:

Sine the component tree is re-created every time the route changes, this will trigger all `componentDidMount` - style effects.

```tsx
import { useRef, useLayoutEffect } from 'preact/hooks'

function AutofocusInput() {
    const ref = useRef<HTMLInputElement|null>(null)
    useLayoutEffect(() => {
        ref.current?.focus()
    }, [])

    return (
        <input ref={ref} />
    )
}
```

# Support / Climate action

This library was made with ☕, 💦 and 💜 by [joshua](https://joshi.monster)
If you really like what you see, you can [Buy me more ☕](https://www.paypal.com/donate/?hosted_button_id=CNT7EUVAM6MZS), or [get in touch](jreusch4+oss@gmail.com)!

If you work on limiting climate change, preserving the environment, et al. and any of my software is useful to you, please let me know if I can help and prioritize issues!