-
-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: donkeyDau <[email protected]> Co-authored-by: Munawwar <[email protected]>
- Loading branch information
1 parent
291fcb1
commit bd83e97
Showing
1 changed file
with
232 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 `<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. | ||
|
||
### `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 `<Router>` 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 = () => ( | ||
<ErrorBoundary> | ||
<Router> | ||
<Home path="/" /> | ||
<Profile path="/profile" /> | ||
</Router> | ||
</ErrorBoundary> | ||
<LocationProvider> | ||
<ErrorBoundary> | ||
<Router> | ||
<Home path="/" /> | ||
{/* Alternative dedicated route component for better TS support */} | ||
<Route path="/profiles" component={Profiles} /> | ||
<Route path="/profiles/:id" component={Profile} /> | ||
{/* `default` prop indicates a fallback route. Useful for 404 pages */} | ||
<NotFound default /> | ||
</Router> | ||
</ErrorBoundary> | ||
</LocationProvider> | ||
); | ||
|
||
render(<App />, 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 = () => ( | ||
<ErrorBoundary> | ||
<Foo path="/" /> | ||
</ErrorBoundary> | ||
<LocationProvider> | ||
<ErrorBoundary> | ||
<Router> | ||
<Foo path="/" /> | ||
</Router> | ||
</ErrorBoundary> | ||
</LocationProvider> | ||
); | ||
|
||
const { html, links } = await prerender(<App />, { maxDepth: 10 }); | ||
hydrate(<App />); | ||
|
||
export async function prerender(data) { | ||
return await ssr(<App />); | ||
} | ||
``` | ||
|
||
### `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 = () => ( | ||
<div class="app"> | ||
<h1>Hello World</h1> | ||
</div> | ||
<LocationProvider> | ||
{/* Your app here */} | ||
</LocationProvider> | ||
); | ||
``` | ||
|
||
hydrate(<App />); | ||
### `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 = () => ( | ||
<LocationProvider> | ||
<Router | ||
onRouteChange={(url) => console.log('Route changed to', url)} | ||
onLoadStart={(url) => console.log('Starting to load', url)} | ||
onLoadEnd={(url) => console.log('Finished loading', url)} | ||
> | ||
<Home path="/" /> | ||
<Profile path="/profile" /> | ||
</Router> | ||
</LocationProvider> | ||
); | ||
``` | ||
|
||
### `router.js` | ||
### `Route` | ||
|
||
|
||
There are two ways to define routes using `preact-iso`: | ||
|
||
1. Append router params to the route components directly: `<Home path="/" />` | ||
2. Use the `Route` component instead: `<Route path="/" component={Home} />` | ||
|
||
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 `<Router>` 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 `<Home>` as taking no props _unless_ it's a child of a `<Router>`, 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 `<Home path="/" />` or we create wrapper components to handle the route definitions. | ||
|
||
While `<Home path="/" />` is completely equivalent to `<Route path="/" component={Home} />`, 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 = () => ( | ||
<LocationProvider> | ||
<Router> | ||
{/* Both of these are equivalent */} | ||
<Home path="/" /> | ||
<Route path="/" component={Home} /> | ||
|
||
<Profile path="/profile" /> | ||
<NotFound default /> | ||
</Router> | ||
</LocationProvider> | ||
); | ||
``` | ||
|
||
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<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' }`) | ||
|
||
### `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 = () => ( | ||
<LocationProvider> | ||
<ErrorBoundary> | ||
<Router> | ||
<Home path="/" /> | ||
<Profile path="/profile" /> | ||
</Router> | ||
</LocationProvider> | ||
); | ||
``` | ||
|
||
### `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 = () => ( | ||
<LocationProvider> | ||
<ErrorBoundary onError={(e) => console.log(e)}> | ||
<Router> | ||
<Home path="/" /> | ||
<Profiles path="/profiles" /> | ||
<Profile path="/profiles/:id" /> | ||
<Profile path="/profile" /> | ||
</Router> | ||
</ErrorBoundary> | ||
</LocationProvider> | ||
); | ||
``` | ||
|
||
During prerendering, the generated HTML includes our full `<Home>` and `<Profiles>` 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 = () => ( | ||
<div class="app"> | ||
<h1>Hello World</h1> | ||
</div> | ||
); | ||
|
||
function ProfileA() { | ||
return <h2>A</h2>; | ||
} | ||
hydrate(<App />); | ||
``` | ||
|
||
function ProfileB() { | ||
return <h2>B</h2>; | ||
} | ||
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 ( | ||
<div> | ||
<h1>Profile</h1> | ||
<ErrorBoundary> | ||
<Router> | ||
<Route path="/a" component={ProfileA} /> | ||
<Route path="/b" component={ProfileB} /> | ||
</Router> | ||
</ErrorBoundary> | ||
</div> | ||
); | ||
} | ||
### `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 = () => ( | ||
<LocationProvider> | ||
<ErrorBoundary> | ||
<Router> | ||
<Route path="/" component={Home} /> | ||
<Route path="/profiles/*" component={Profile} /> | ||
<Foo path="/" /> | ||
<Bar path="/bar" /> | ||
</Router> | ||
</ErrorBoundary> | ||
</LocationProvider> | ||
); | ||
|
||
const { html, links } = await prerender(<App />); | ||
``` | ||
|
||
## License | ||
|
||
[MIT](./LICENSE) |