Skip to content

docs: Add nested router example, format, clear up couple of issues #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 7, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 85 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

Isomorphic async tools for Preact.

- Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration.
- Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies.
- Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions.
- Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration.
- Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies.
- Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions.

## Routing

Expand All @@ -30,7 +30,7 @@ const App = () => (
<Home path="/" />
{/* Alternative dedicated route component for better TS support */}
<Route path="/profiles" component={Profiles} />
<Route path="/profiles/:id" component={Profile} />
<Route path="/profile/:id" component={Profile} />
{/* `default` prop indicates a fallback route. Useful for 404 pages */}
<NotFound default />
</Router>
Expand All @@ -41,9 +41,7 @@ const App = () => (

**Progressive Hydration:** When the app is hydrated on the client, the route (`Home` or `Profile` in this case) suspends. This causes hydration for that part of the page to be deferred until the route's `import()` is resolved, at which point that part of the page automatically finishes hydrating.

**Seamless Routing:** Switch switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route (or its data), the router preserves the current route in-place until the incoming route has finished loading, then they are swapped.

**Nested Routing:** Nested routes are supported by using multiple `Router` components. Partially matched routes end with a wildcard `/*` and the remaining value will be passed to continue matching with if there are any further routes.
**Seamless Routing:** When switching between routes on the client, the Router is aware of asynchronous dependencies in routes. Instead of clearing the current route and showing a loading spinner while waiting for the next route, the router preserves the current route in-place until the incoming route has finished loading, then they are swapped.

## Prerendering

Expand Down Expand Up @@ -74,6 +72,46 @@ export async function prerender(data) {
}
```

## Nested Routing

Nested routes are supported by using multiple `Router` components. Partially matched routes end with a wildcard (`/*`) and the remaining value will be passed to continue matching with if there are any further routes.

```js
import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';

const NotFound = lazy(() => import('./routes/_404.js'));

const App = () => (
<LocationProvider>
<ErrorBoundary>
<Router>
<Route path="/movies/*" component={Movies} />
<NotFound default />
</Router>
</ErrorBoundary>
</LocationProvider>
);

const TrendingMovies = lazy(() => import('./routes/movies/trending.js'));
const SearchMovies = lazy(() => import('./routes/movies/search.js'));
const MovieDetails = lazy(() => import('./routes/movies/details.js'));

const Movies = () => (
<ErrorBoundary>
<Router>
<Route path="/trending" component={TrendingMovies} />
<Route path="/search" component={SearchMovies} />
<Route path="/:id" component={MovieDetails} />
</Router>
</ErrorBoundary>
);
```

This will match the following routes:
- `/movies/trending`
- `/movies/search`
- `/movies/Inception`

---

## API Docs
Expand All @@ -84,7 +122,7 @@ A context provider that provides the current location to its children. This is r

Props:

- `scope?: string | RegExp` - Sets a scope for the paths that the router will handle (intercept). If a path does not match the scope, either by starting with the provided string or matching the RegExp, the router will ignore it and default browser navigation will apply.
- `scope?: string | RegExp` - Sets a scope for the paths that the router will handle (intercept). If a path does not match the scope, either by starting with the provided string or matching the RegExp, the router will ignore it and default browser navigation will apply.

Typically, you would wrap your entire app in this provider:

Expand All @@ -102,9 +140,9 @@ const App = () => (

Props:

- `onRouteChange?: (url: string) => void` - Callback to be called when a route changes.
- `onLoadStart?: (url: string) => void` - Callback to be called when a route starts loading (i.e., if it suspends). This will not be called before navigations to sync routes or subsequent navigations to async routes.
- `onLoadEnd?: (url: string) => void` - Callback to be called after a route finishes loading (i.e., if it suspends). This will not be called after navigations to sync routes or subsequent navigations to async routes.
- `onRouteChange?: (url: string) => void` - Callback to be called when a route changes.
- `onLoadStart?: (url: string) => void` - Callback to be called when a route starts loading (i.e., if it suspends). This will not be called before navigations to sync routes or subsequent navigations to async routes.
- `onLoadEnd?: (url: string) => void` - Callback to be called after a route finishes loading (i.e., if it suspends). This will not be called after navigations to sync routes or subsequent navigations to async routes.

```js
import { LocationProvider, Router } from 'preact-iso';
Expand All @@ -117,7 +155,8 @@ const App = () => (
onLoadEnd={(url) => console.log('Finished loading', url)}
>
<Home path="/" />
<Profile path="/profile" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
Expand Down Expand Up @@ -147,7 +186,8 @@ const App = () => (
<Home path="/" />
<Route path="/" component={Home} />

<Profile path="/profile" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
<NotFound default />
</Router>
</LocationProvider>
Expand All @@ -156,34 +196,34 @@ const App = () => (

Props for any route component:

- `path: string` - The path to match (read on)
- `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches
- `path: string` - The path to match (read on)
- `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches

Specific to the `Route` component:

- `component: AnyComponent` - The component to render when the route matches
- `component: AnyComponent` - The component to render when the route matches

#### Path Segment Matching

Paths are matched using a simple string matching algorithm. The following features may be used:

- `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`)
- `/profile/:id` will match `/profile/123` and `/profile/abc`
- `/profile/:id?` will match `/profile` and `/profile/123`
- `/profile/:id*` will match `/profile`, `/profile/123`, and `/profile/123/abc`
- `/profile/:id+` will match `/profile/123`, `/profile/123/abc`
- `*` - Matches one or more URL segments
- `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc.
- `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`)
- `/profile/:id` will match `/profile/123` and `/profile/abc`
- `/profile/:id?` will match `/profile` and `/profile/123`
- `/profile/:id*` will match `/profile`, `/profile/123`, and `/profile/123/abc`
- `/profile/:id+` will match `/profile/123`, `/profile/123/abc`
- `*` - Matches one or more URL segments
- `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc.

These can then be composed to create more complex routes:

- `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc.
- `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc.

The difference between `/:id*` and `/:id/*` is that in the former, the `id` param will include the entire path after it, while in the latter, the `id` is just the single path segment.

- `/profile/:id*`, with `/profile/123/abc`
- `id` is `123/abc`
- `/profile/:id/*`, with `/profile/123/abc`
- `/profile/:id*`, with `/profile/123/abc`
- `id` is `123/abc`
- `/profile/:id/*`, with `/profile/123/abc`
- `id` is `123`

### `useLocation`
Expand All @@ -192,21 +232,20 @@ A hook to work with the `LocationProvider` to access location context.

Returns an object with the following properties:

- `url: string` - The current path & search params
- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `route: (url: string, replace?: boolean) => void` - A function to programmatically navigate to a new route. The `replace` param can optionally be used to overwrite history, navigating them away without keeping the current location in the history stack.
- `url: string` - The current path & search params
- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `route: (url: string, replace?: boolean) => void` - A function to programmatically navigate to a new route. The `replace` param can optionally be used to overwrite history, navigating them away without keeping the current location in the history stack.

### `useRoute`

A hook to access current route information. Unlike `useLocation`, this hook only works within `<Router>` components.

Returns an object with the following properties:


- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `params: Record<string, string>` - The current route parameters (`/profile/:id` -> `{ id: '123' }`)
- `path: string` - The current path
- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`)
- `params: Record<string, string>` - The current route parameters (`/profile/:id` -> `{ id: '123' }`)

### `lazy`

Expand All @@ -221,13 +260,15 @@ import { lazy, LocationProvider, Router } from 'preact-iso';
import Home from './routes/home.js';

// Asynchronous, code-splitted:
const Profile = lazy(() => import('./routes/profile.js'));
const Profiles = lazy(() => import('./routes/profiles.js').then(m => m.Profiles)); // Expects a named export called `Profiles`
const Profile = lazy(() => import('./routes/profile.js')); // Expects a default export

const App = () => (
<LocationProvider>
<Router>
<Home path="/" />
<Profile path="/profile" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</LocationProvider>
);
Expand All @@ -240,7 +281,7 @@ const Profile = lazy(() => import('./routes/profile.js'));

function Home() {
return (
<a href="/profile" onMouseOver={() => Profile.preload()}>
<a href="/profile/rschristian" onMouseOver={() => Profile.preload()}>
Profile Page -- Hover over me to preload the module!
</a>
);
Expand All @@ -253,7 +294,7 @@ A simple component to catch errors in the component tree below it.

Props:

- `onError?: (error: Error) => void` - A callback to be called when an error is caught
- `onError?: (error: Error) => void` - A callback to be called when an error is caught

```js
import { LocationProvider, ErrorBoundary, Router } from 'preact-iso';
Expand All @@ -263,7 +304,8 @@ const App = () => (
<ErrorBoundary onError={(e) => console.log(e)}>
<Router>
<Home path="/" />
<Profile path="/profile" />
<Profiles path="/profiles" />
<Profile path="/profile/:id" />
</Router>
</ErrorBoundary>
</LocationProvider>
Expand All @@ -278,8 +320,8 @@ Pairs with the `prerender()` function.

Params:

- `jsx: ComponentChild` - The JSX element or component to render
- `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided.
- `jsx: ComponentChild` - The JSX element or component to render
- `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided.

```js
import { hydrate } from 'preact-iso';
Expand All @@ -303,7 +345,7 @@ Pairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-

Params:

- `jsx: ComponentChild` - The JSX element or component to render
- `jsx: ComponentChild` - The JSX element or component to render

```js
import { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso';
Expand Down