Skip to content

Commit bd83e97

Browse files
rschristiandonkeyDauMunawwar
authored
docs: Rewrite docs (#25)
Co-authored-by: donkeyDau <[email protected]> Co-authored-by: Munawwar <[email protected]>
1 parent 291fcb1 commit bd83e97

File tree

1 file changed

+232
-73
lines changed

1 file changed

+232
-73
lines changed

README.md

Lines changed: 232 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,306 @@
11
# preact-iso
22

3+
[![Preact Slack Community](https://img.shields.io/badge/slack-Preact%20Slack%20Community-blue?logo=slack)](https://chat.preactjs.com/)
4+
35
Isomorphic async tools for Preact.
46

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

9-
### `lazy.js`
11+
## Routing
1012

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

1415
```js
15-
import { render } from 'preact';
16-
import { ErrorBoundary, lazy, Router } from 'preact-iso';
16+
import { lazy, LocationProvider, ErrorBoundary, Router, Route } from 'preact-iso';
1717

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';
2120

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'));
2423
const Profile = lazy(() => import('./routes/profile.js'));
24+
const NotFound = lazy(() => import('./routes/_404.js'));
2525

2626
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>
3339
);
34-
35-
render(<App />, document.body);
3640
```
3741

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

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

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

4454
```js
45-
import { ErrorBoundary, lazy, prerender } from 'preact-iso';
55+
import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso';
4656

4757
// Asynchronous (throws a promise)
4858
const Foo = lazy(() => import('./foo.js'));
4959

5060
const App = () => (
51-
<ErrorBoundary>
52-
<Foo path="/" />
53-
</ErrorBoundary>
61+
<LocationProvider>
62+
<ErrorBoundary>
63+
<Router>
64+
<Foo path="/" />
65+
</Router>
66+
</ErrorBoundary>
67+
</LocationProvider>
5468
);
5569

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+
}
5775
```
5876

59-
### `hydrate.js`
77+
---
78+
79+
## API Docs
6080

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

6387
```js
64-
import { hydrate } from 'preact-iso';
88+
import { LocationProvider } from 'preact-iso';
6589

6690
const App = () => (
67-
<div class="app">
68-
<h1>Hello World</h1>
69-
</div>
91+
<LocationProvider>
92+
{/* Your app here */}
93+
</LocationProvider>
7094
);
95+
```
7196

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+
);
73120
```
74121

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

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

79136
```js
80-
import { ErrorBoundary, lazy, LocationProvider, Router, useLocation } from 'preact-iso';
137+
import { LocationProvider, Router, Route } from 'preact-iso';
81138

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:
83212
const Home = lazy(() => import('./routes/home.js'));
84213
const Profile = lazy(() => import('./routes/profile.js'));
85-
const Profiles = lazy(() => import('./routes/profiles.js'));
86214

87215
const App = () => (
88216
<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)}>
90239
<Router>
91240
<Home path="/" />
92-
<Profiles path="/profiles" />
93-
<Profile path="/profiles/:id" />
241+
<Profile path="/profile" />
94242
</Router>
95243
</ErrorBoundary>
96244
</LocationProvider>
97245
);
98246
```
99247

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`
101249

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

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

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

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

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';
111261

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+
);
114267

115-
function ProfileA() {
116-
return <h2>A</h2>;
117-
}
268+
hydrate(<App />);
269+
```
118270

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

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'));
136289

137290
const App = () => (
138291
<LocationProvider>
139292
<ErrorBoundary>
140293
<Router>
141-
<Route path="/" component={Home} />
142-
<Route path="/profiles/*" component={Profile} />
294+
<Foo path="/" />
295+
<Bar path="/bar" />
143296
</Router>
144297
</ErrorBoundary>
145298
</LocationProvider>
146299
);
300+
301+
const { html, links } = await prerender(<App />);
147302
```
303+
304+
## License
305+
306+
[MIT](./LICENSE)

0 commit comments

Comments
 (0)