@jreusch/router-preact
TypeScript icon, indicating that this package has built-in type declarations

1.0.5 • Public • Published

Preact library for @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:

npm install @jreusch/router-preact

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

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:

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 in the core API documentation.

Components

<PathRouter>, <HashRouter>, <MemoryRouter>, <PathWithBaseRouter>

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, so you can also go there to learn more about their differences!

The different flavors of routers change which internal router object 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.

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 if you need to switch between different Router components depending on the context.

<Route>

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 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 instead!

(
    {/* 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.

<AsyncRoute>

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) 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).

(
    <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.

<Redirect>

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.

(
    // 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.

<Link>

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.

(
    <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 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:

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 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 on how patterns, segments and matching works. I highly recommend to check it out!

The entire API of @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 as a prop, making it possible to dynamically switch between them:

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 and that). 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.

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 If you really like what you see, you can Buy me more ☕, or get in touch!

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!

Readme

Keywords

Package Sidebar

Install

npm i @jreusch/router-preact

Weekly Downloads

0

Version

1.0.5

License

BSD-3-Clause

Unpacked Size

39.2 kB

Total Files

15

Last publish

Collaborators

  • jreusch