|
1 | 1 | # preact-iso
|
2 | 2 |
|
| 3 | +[](https://chat.preactjs.com/) |
| 4 | + |
3 | 5 | Isomorphic async tools for Preact.
|
4 | 6 |
|
5 |
| -- Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration. |
6 |
| -- Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies. |
7 |
| -- Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions. |
| 7 | +- Lazy-load components using `lazy()` and `<ErrorBoundary>`, which also enables progressive hydration. |
| 8 | +- Generate static HTML for your app using `prerender()`, waiting for `lazy()` components and data dependencies. |
| 9 | +- Implement async-aware client and server-side routing using `<Router>`, including seamless async transitions. |
8 | 10 |
|
9 |
| -### `lazy.js` |
| 11 | +## Routing |
10 | 12 |
|
11 |
| -Make a lazily-loaded version of a Component. |
12 |
| -`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. |
| 13 | +`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. |
13 | 14 |
|
14 | 15 | ```js
|
15 |
| -import { render } from 'preact'; |
16 |
| -import { ErrorBoundary, lazy, Router } from 'preact-iso'; |
| 16 | +import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso'; |
17 | 17 |
|
18 |
| -// Synchronous, not code-splitted: |
19 |
| -// import Home from './routes/home.js'; |
20 |
| -// import Profile from './routes/profile.js'; |
| 18 | +// Synchronous |
| 19 | +import Home from './routes/home.js'; |
21 | 20 |
|
22 |
| -// Asynchronous, code-splitted: |
23 |
| -const Home = lazy(() => import('./routes/home.js')); |
| 21 | +// Asynchronous (throws a promise) |
| 22 | +const Profiles = lazy(() => import('./routes/profiles.js')); |
24 | 23 | const Profile = lazy(() => import('./routes/profile.js'));
|
| 24 | +const NotFound = lazy(() => import('./routes/_404.js')); |
25 | 25 |
|
26 | 26 | const App = () => (
|
27 |
| - <ErrorBoundary> |
28 |
| - <Router> |
29 |
| - <Home path="/" /> |
30 |
| - <Profile path="/profile" /> |
31 |
| - </Router> |
32 |
| - </ErrorBoundary> |
| 27 | + <LocationProvider> |
| 28 | + <ErrorBoundary> |
| 29 | + <Router> |
| 30 | + <Home path="/" /> |
| 31 | + {/* Alternative dedicated route component for better TS support */} |
| 32 | + <Route path="/profiles" component={Profiles} /> |
| 33 | + <Route path="/profiles/:id" component={Profile} /> |
| 34 | + {/* `default` prop indicates a fallback route. Useful for 404 pages */} |
| 35 | + <NotFound default /> |
| 36 | + </Router> |
| 37 | + </ErrorBoundary> |
| 38 | + </LocationProvider> |
33 | 39 | );
|
34 |
| - |
35 |
| -render(<App />, document.body); |
36 | 40 | ```
|
37 | 41 |
|
38 |
| -### `prerender.js` |
| 42 | +**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. |
39 | 43 |
|
40 |
| -`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`. |
| 44 | +**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. |
41 | 45 |
|
42 |
| -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. |
| 46 | +**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. |
| 47 | + |
| 48 | +## Prerendering |
| 49 | + |
| 50 | +`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. |
| 51 | + |
| 52 | +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. |
43 | 53 |
|
44 | 54 | ```js
|
45 |
| -import { ErrorBoundary, lazy, prerender } from 'preact-iso'; |
| 55 | +import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso'; |
46 | 56 |
|
47 | 57 | // Asynchronous (throws a promise)
|
48 | 58 | const Foo = lazy(() => import('./foo.js'));
|
49 | 59 |
|
50 | 60 | const App = () => (
|
51 |
| - <ErrorBoundary> |
52 |
| - <Foo path="/" /> |
53 |
| - </ErrorBoundary> |
| 61 | + <LocationProvider> |
| 62 | + <ErrorBoundary> |
| 63 | + <Router> |
| 64 | + <Foo path="/" /> |
| 65 | + </Router> |
| 66 | + </ErrorBoundary> |
| 67 | + </LocationProvider> |
54 | 68 | );
|
55 | 69 |
|
56 |
| -const { html, links } = await prerender(<App />, { maxDepth: 10 }); |
| 70 | +hydrate(<App />); |
| 71 | + |
| 72 | +export async function prerender(data) { |
| 73 | + return await ssr(<App />); |
| 74 | +} |
57 | 75 | ```
|
58 | 76 |
|
59 |
| -### `hydrate.js` |
| 77 | +--- |
| 78 | + |
| 79 | +## API Docs |
60 | 80 |
|
61 |
| -`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. |
| 81 | +### `LocationProvider` |
| 82 | + |
| 83 | +A context provider that provides the current location to its children. This is required for the router to function. |
| 84 | + |
| 85 | +Typically, you would wrap your entire app in this provider: |
62 | 86 |
|
63 | 87 | ```js
|
64 |
| -import { hydrate } from 'preact-iso'; |
| 88 | +import { LocationProvider } from 'preact-iso'; |
65 | 89 |
|
66 | 90 | const App = () => (
|
67 |
| - <div class="app"> |
68 |
| - <h1>Hello World</h1> |
69 |
| - </div> |
| 91 | + <LocationProvider> |
| 92 | + {/* Your app here */} |
| 93 | + </LocationProvider> |
70 | 94 | );
|
| 95 | +``` |
71 | 96 |
|
72 |
| -hydrate(<App />); |
| 97 | +### `Router` |
| 98 | + |
| 99 | +Props: |
| 100 | + |
| 101 | +- `onRouteChange?: (url: string) => void` - Callback to be called when a route changes. |
| 102 | +- `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. |
| 103 | +- `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. |
| 104 | + |
| 105 | +```js |
| 106 | +import { LocationProvider, Router } from 'preact-iso'; |
| 107 | + |
| 108 | +const App = () => ( |
| 109 | + <LocationProvider> |
| 110 | + <Router |
| 111 | + onRouteChange={(url) => console.log('Route changed to', url)} |
| 112 | + onLoadStart={(url) => console.log('Starting to load', url)} |
| 113 | + onLoadEnd={(url) => console.log('Finished loading', url)} |
| 114 | + > |
| 115 | + <Home path="/" /> |
| 116 | + <Profile path="/profile" /> |
| 117 | + </Router> |
| 118 | + </LocationProvider> |
| 119 | +); |
73 | 120 | ```
|
74 | 121 |
|
75 |
| -### `router.js` |
| 122 | +### `Route` |
| 123 | + |
| 124 | + |
| 125 | +There are two ways to define routes using `preact-iso`: |
| 126 | + |
| 127 | +1. Append router params to the route components directly: `<Home path="/" />` |
| 128 | +2. Use the `Route` component instead: `<Route path="/" component={Home} />` |
| 129 | + |
| 130 | +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. |
76 | 131 |
|
77 |
| -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. |
| 132 | +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. |
| 133 | + |
| 134 | +While `<Home path="/" />` is completely equivalent to `<Route path="/" component={Home} />`, TS users may find the latter preferable. |
78 | 135 |
|
79 | 136 | ```js
|
80 |
| -import { ErrorBoundary, lazy, LocationProvider, Router, useLocation } from 'preact-iso'; |
| 137 | +import { LocationProvider, Router, Route } from 'preact-iso'; |
81 | 138 |
|
82 |
| -// Asynchronous (throws a promise) |
| 139 | +const App = () => ( |
| 140 | + <LocationProvider> |
| 141 | + <Router> |
| 142 | + {/* Both of these are equivalent */} |
| 143 | + <Home path="/" /> |
| 144 | + <Route path="/" component={Home} /> |
| 145 | + |
| 146 | + <Profile path="/profile" /> |
| 147 | + <NotFound default /> |
| 148 | + </Router> |
| 149 | + </LocationProvider> |
| 150 | +); |
| 151 | +``` |
| 152 | + |
| 153 | +Props for any route component: |
| 154 | + |
| 155 | +- `path: string` - The path to match (read on) |
| 156 | +- `default?: boolean` - If set, this route is a fallback/default route to be used when nothing else matches |
| 157 | + |
| 158 | +Specific to the `Route` component: |
| 159 | + |
| 160 | +- `component: AnyComponent` - The component to render when the route matches |
| 161 | + |
| 162 | +#### Path Segment Matching |
| 163 | + |
| 164 | +Paths are matched using a simple string matching algorithm. The following features may be used: |
| 165 | + |
| 166 | +- `:param` - Matches any URL segment, binding the value to the label (can later extract this value from `useRoute()`) |
| 167 | + - `/profile/:id` will match `/profile/123` and `/profile/abc` |
| 168 | + - `/profile/:id?` will match `/profile` and `/profile/123` |
| 169 | +- `*` - Matches one or more URL segments |
| 170 | + - `/profile/*` will match `/profile/123`, `/profile/123/abc`, etc. |
| 171 | + |
| 172 | +These can then be composed to create more complex routes: |
| 173 | + |
| 174 | +- `/profile/:id/*` will match `/profile/123/abc`, `/profile/123/abc/def`, etc. |
| 175 | + |
| 176 | +### `useLocation` |
| 177 | + |
| 178 | +A hook to work with the `LocationProvider` to access location context. |
| 179 | + |
| 180 | +Returns an object with the following properties: |
| 181 | + |
| 182 | +- `url: string` - _Redundant_ - The current path |
| 183 | +- `path: string` - The current path |
| 184 | +- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`) |
| 185 | +- `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. |
| 186 | + |
| 187 | +### `useRoute` |
| 188 | + |
| 189 | +A hook to access current route information. Unlike `useLocation`, this hook only works within `<Router>` components. |
| 190 | + |
| 191 | +Returns an object with the following properties: |
| 192 | + |
| 193 | + |
| 194 | +- `path: string` - The current path |
| 195 | +- `query: Record<string, string>` - The current query string parameters (`/profile?name=John` -> `{ name: 'John' }`) |
| 196 | +- `params: Record<string, string>` - The current route parameters (`/profile/:id` -> `{ id: '123' }`) |
| 197 | + |
| 198 | +### `lazy` |
| 199 | + |
| 200 | +Make a lazily-loaded version of a Component. |
| 201 | + |
| 202 | +`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. |
| 203 | + |
| 204 | +```js |
| 205 | +import { lazy, LocationProvider, Router } from 'preact-iso'; |
| 206 | + |
| 207 | +// Synchronous, not code-splitted: |
| 208 | +// import Home from './routes/home.js'; |
| 209 | +// import Profile from './routes/profile.js'; |
| 210 | + |
| 211 | +// Asynchronous, code-splitted: |
83 | 212 | const Home = lazy(() => import('./routes/home.js'));
|
84 | 213 | const Profile = lazy(() => import('./routes/profile.js'));
|
85 |
| -const Profiles = lazy(() => import('./routes/profiles.js')); |
86 | 214 |
|
87 | 215 | const App = () => (
|
88 | 216 | <LocationProvider>
|
89 |
| - <ErrorBoundary> |
| 217 | + <Router> |
| 218 | + <Home path="/" /> |
| 219 | + <Profile path="/profile" /> |
| 220 | + </Router> |
| 221 | + </LocationProvider> |
| 222 | +); |
| 223 | +``` |
| 224 | + |
| 225 | +### `ErrorBoundary` |
| 226 | + |
| 227 | +A simple component to catch errors in the component tree below it. |
| 228 | + |
| 229 | +Props: |
| 230 | + |
| 231 | +- `onError?: (error: Error) => void` - A callback to be called when an error is caught |
| 232 | + |
| 233 | +```js |
| 234 | +import { LocationProvider, ErrorBoundary, Router } from 'preact-iso'; |
| 235 | + |
| 236 | +const App = () => ( |
| 237 | + <LocationProvider> |
| 238 | + <ErrorBoundary onError={(e) => console.log(e)}> |
90 | 239 | <Router>
|
91 | 240 | <Home path="/" />
|
92 |
| - <Profiles path="/profiles" /> |
93 |
| - <Profile path="/profiles/:id" /> |
| 241 | + <Profile path="/profile" /> |
94 | 242 | </Router>
|
95 | 243 | </ErrorBoundary>
|
96 | 244 | </LocationProvider>
|
97 | 245 | );
|
98 | 246 | ```
|
99 | 247 |
|
100 |
| -During prerendering, the generated HTML includes our full `<Home>` and `<Profiles>` component output because it waits for the `lazy()`-wrapped `import()` to resolve. |
| 248 | +### `hydrate` |
101 | 249 |
|
102 |
| -You can use the `useRoute` hook to get information of the route you are currently on. |
| 250 | +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. |
103 | 251 |
|
104 |
| -**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. |
| 252 | +Pairs with the `prerender()` function. |
105 | 253 |
|
106 |
| -**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. |
| 254 | +Params: |
107 | 255 |
|
108 |
| -### Nested Routing |
| 256 | +- `jsx: ComponentChild` - The JSX element or component to render |
| 257 | +- `parent?: Element | Document | ShadowRoot | DocumentFragment` - The parent element to render into. Defaults to `document.body` if not provided. |
109 | 258 |
|
110 |
| -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. |
| 259 | +```js |
| 260 | +import { hydrate } from 'preact-iso'; |
111 | 261 |
|
112 |
| -```jsx |
113 |
| -import { ErrorBoundary, LocationProvider, Router, Route } from 'preact-iso'; |
| 262 | +const App = () => ( |
| 263 | + <div class="app"> |
| 264 | + <h1>Hello World</h1> |
| 265 | + </div> |
| 266 | +); |
114 | 267 |
|
115 |
| -function ProfileA() { |
116 |
| - return <h2>A</h2>; |
117 |
| -} |
| 268 | +hydrate(<App />); |
| 269 | +``` |
118 | 270 |
|
119 |
| -function ProfileB() { |
120 |
| - return <h2>B</h2>; |
121 |
| -} |
| 271 | +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. |
122 | 272 |
|
123 |
| -function Profile() { |
124 |
| - return ( |
125 |
| - <div> |
126 |
| - <h1>Profile</h1> |
127 |
| - <ErrorBoundary> |
128 |
| - <Router> |
129 |
| - <Route path="/a" component={ProfileA} /> |
130 |
| - <Route path="/b" component={ProfileB} /> |
131 |
| - </Router> |
132 |
| - </ErrorBoundary> |
133 |
| - </div> |
134 |
| - ); |
135 |
| -} |
| 273 | +### `prerender` |
| 274 | + |
| 275 | +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. |
| 276 | + |
| 277 | +Pairs primarily with [`@preact/preset-vite`](https://github.com/preactjs/preset-vite#prerendering-configuration)'s prerendering. |
| 278 | + |
| 279 | +Params: |
| 280 | + |
| 281 | +- `jsx: ComponentChild` - The JSX element or component to render |
| 282 | + |
| 283 | +```js |
| 284 | +import { LocationProvider, ErrorBoundary, Router, lazy, prerender } from 'preact-iso'; |
| 285 | + |
| 286 | +// Asynchronous (throws a promise) |
| 287 | +const Foo = lazy(() => import('./foo.js')); |
| 288 | +const Bar = lazy(() => import('./bar.js')); |
136 | 289 |
|
137 | 290 | const App = () => (
|
138 | 291 | <LocationProvider>
|
139 | 292 | <ErrorBoundary>
|
140 | 293 | <Router>
|
141 |
| - <Route path="/" component={Home} /> |
142 |
| - <Route path="/profiles/*" component={Profile} /> |
| 294 | + <Foo path="/" /> |
| 295 | + <Bar path="/bar" /> |
143 | 296 | </Router>
|
144 | 297 | </ErrorBoundary>
|
145 | 298 | </LocationProvider>
|
146 | 299 | );
|
| 300 | + |
| 301 | +const { html, links } = await prerender(<App />); |
147 | 302 | ```
|
| 303 | + |
| 304 | +## License |
| 305 | + |
| 306 | +[MIT](./LICENSE) |
0 commit comments