diff --git a/README.md b/README.md index 503bbac..715491c 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,306 @@ # preact-iso +[![Preact Slack Community](https://img.shields.io/badge/slack-Preact%20Slack%20Community-blue?logo=slack)](https://chat.preactjs.com/) + Isomorphic async tools for Preact. -- Lazy-load components using `lazy()` and ``, 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 ``, including seamless async transitions. +- Lazy-load components using `lazy()` and ``, 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 ``, including seamless async transitions. -### `lazy.js` +## Routing -Make a lazily-loaded version of a Component. -`lazy()` takes an async function that resolves to a Component, and returns a wrapper version of that Component. The wrapper component can be rendered right away, even though the component is only loaded the first time it is rendered. +`preact-iso` offers a simple router for Preact with conventional and hooks-based APIs. The `` component is async-aware: when transitioning from one route to another, if the incoming route suspends (throws a Promise), the outgoing route is preserved until the new one becomes ready. ```js -import { render } from 'preact'; -import { ErrorBoundary, lazy, Router } from 'preact-iso'; +import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso'; -// Synchronous, not code-splitted: -// import Home from './routes/home.js'; -// import Profile from './routes/profile.js'; +// Synchronous +import Home from './routes/home.js'; -// Asynchronous, code-splitted: -const Home = lazy(() => import('./routes/home.js')); +// Asynchronous (throws a promise) +const Profiles = lazy(() => import('./routes/profiles.js')); const Profile = lazy(() => import('./routes/profile.js')); +const NotFound = lazy(() => import('./routes/_404.js')); const App = () => ( - - - - - - + + + + + {/* Alternative dedicated route component for better TS support */} + + + {/* `default` prop indicates a fallback route. Useful for 404 pages */} + + + + ); - -render(, document.body); ``` -### `prerender.js` +**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. -`prerender()` renders a Virtual DOM tree to an HTML string using [preact-render-to-string](https://github.com/preactjs/preact-render-to-string). The difference is that it is asynchronous, and waits for any Promises thrown by components during rendering (Suspense-style) to resolve before returning the HTML. Nested promises also work, and the maximum depth can be controlled using the `maxDepth` option, which defaults to `10`. +**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. -The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page. +**Nested Routing:** Nested routes are supported by using multiple `Router` components. Partially matched routes end with a wildcard `/*` and the remaining value will be past to continue matching with if there are any further routes. + +## Prerendering + +`prerender()` renders a Virtual DOM tree to an HTML string using [`preact-render-to-string`](https://github.com/preactjs/preact-render-to-string). The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page. + +Primarily meant for use with prerendering via [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration) or other prerendering systems that share the API. If you're server-side rendering your app via any other method, you can use `preact-render-to-string` (specifically `renderToStringAsync()`) directly. ```js -import { ErrorBoundary, lazy, prerender } from 'preact-iso'; +import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso'; // Asynchronous (throws a promise) const Foo = lazy(() => import('./foo.js')); const App = () => ( - - - + + + + + + + ); -const { html, links } = await prerender(, { maxDepth: 10 }); +hydrate(); + +export async function prerender(data) { + return await ssr(); +} ``` -### `hydrate.js` +--- + +## API Docs -`hydrate()` is a thin wrapper around Preact's hydrate() method. It performs hydration when the HTML for the current page includes pre-rendered output from `prerender()`. It falls back to plain rendering in any other cases, which is useful if you're not pre-rendering during development. This method also checks to make sure its running in a browser context before attempting any rendering - if not, it does nothing. +### `LocationProvider` + +A context provider that provides the current location to its children. This is required for the router to function. + +Typically, you would wrap your entire app in this provider: ```js -import { hydrate } from 'preact-iso'; +import { LocationProvider } from 'preact-iso'; const App = () => ( -
-

Hello World

-
+ + {/* Your app here */} + ); +``` -hydrate(); +### `Router` + +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. + +```js +import { LocationProvider, Router } from 'preact-iso'; + +const App = () => ( + + console.log('Route changed to', url)} + onLoadStart={(url) => console.log('Starting to load', url)} + onLoadEnd={(url) => console.log('Finished loading', url)} + > + + + + +); ``` -### `router.js` +### `Route` + + +There are two ways to define routes using `preact-iso`: + +1. Append router params to the route components directly: `` +2. Use the `Route` component instead: `` + +Appending arbitrary props to components not unreasonable in JavaScript, as JS is a dynamic language that's perfectly happy to support dynamic & arbitrary interfaces. However, TypeScript, which many of us use even when writing JS (via TS's language server), is not exactly a fan of this sort of interface design. -A simple router for Preact with conventional and hooks-based APIs. The `` component is async-aware: when transitioning from one route to another, if the incoming route suspends (throws a Promise), the outgoing route is preserved until the new one becomes ready. +TS does not (yet) allow for overriding a child's props from the parent component so we cannot, for instance, define `` as taking no props _unless_ it's a child of a ``, in which case it can have a `path` prop. This leaves us with a bit of a dilemma: either we define all of our routes as taking `path` props so we don't see TS errors when writing `` or we create wrapper components to handle the route definitions. + +While `` is completely equivalent to ``, TS users may find the latter preferable. ```js -import { ErrorBoundary, lazy, LocationProvider, Router, useLocation } from 'preact-iso'; +import { LocationProvider, Router, Route } from 'preact-iso'; -// Asynchronous (throws a promise) +const App = () => ( + + + {/* Both of these are equivalent */} + + + + + + + +); +``` + +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 + +Specific to the `Route` component: + +- `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` +- `*` - 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. + +### `useLocation` + +A hook to work with the `LocationProvider` to access location context. + +Returns an object with the following properties: + +- `url: string` - _Redundant_ - The current path +- `path: string` - The current path +- `query: Record` - 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 `` components. + +Returns an object with the following properties: + + +- `path: string` - The current path +- `query: Record` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`) +- `params: Record` - The current route parameters (`/profile/:id` -> `{ id: '123' }`) + +### `lazy` + +Make a lazily-loaded version of a Component. + +`lazy()` takes an async function that resolves to a Component, and returns a wrapper version of that Component. The wrapper component can be rendered right away, even though the component is only loaded the first time it is rendered. + +```js +import { lazy, LocationProvider, Router } from 'preact-iso'; + +// Synchronous, not code-splitted: +// import Home from './routes/home.js'; +// import Profile from './routes/profile.js'; + +// Asynchronous, code-splitted: const Home = lazy(() => import('./routes/home.js')); const Profile = lazy(() => import('./routes/profile.js')); -const Profiles = lazy(() => import('./routes/profiles.js')); const App = () => ( - + + + + + +); +``` + +### `ErrorBoundary` + +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 + +```js +import { LocationProvider, ErrorBoundary, Router } from 'preact-iso'; + +const App = () => ( + + console.log(e)}> - - + ); ``` -During prerendering, the generated HTML includes our full `` and `` component output because it waits for the `lazy()`-wrapped `import()` to resolve. +### `hydrate` -You can use the `useRoute` hook to get information of the route you are currently on. +A thin wrapper around Preact's `hydrate` export, it switches between hydrating and rendering the provided element, depending on whether the current page has been prerendered. Additionally, it checks to ensure it's running in a browser context before attempting any rendering, making it a no-op during SSR. -**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. +Pairs with the `prerender()` function. -**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. +Params: -### Nested Routing +- `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. -Nested routes are supported by using multiple `Router` components. Partially matched routes end with a wildcard `/*` and the remaining value will be past to continue matching with if there are any further routes. +```js +import { hydrate } from 'preact-iso'; -```jsx -import { ErrorBoundary, LocationProvider, Router, Route } from 'preact-iso'; +const App = () => ( +
+

Hello World

+
+); -function ProfileA() { - return

A

; -} +hydrate(); +``` -function ProfileB() { - return

B

; -} +However, it is just a simple utility method. By no means is it essential to use, you can always use Preact's `hydrate` export directly. -function Profile() { - return ( -
-

Profile

- - - - - - -
- ); -} +### `prerender` + +Renders a Virtual DOM tree to an HTML string using `preact-render-to-string`. The Promise returned from `prerender()` resolves to an Object with `html` and `links[]` properties. The `html` property contains your pre-rendered static HTML markup, and `links` is an Array of any non-external URL strings found in links on the generated page. + +Pairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration)'s prerendering. + +Params: + +- `jsx: ComponentChild` - The JSX element or component to render + +```js +import { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso'; + +// Asynchronous (throws a promise) +const Foo = lazy(() => import('./foo.js')); +const Bar = lazy(() => import('./bar.js')); const App = () => ( - - + + ); + +const { html, links } = await prerender(); ``` + +## License + +[MIT](./LICENSE)