diff --git a/README.md b/README.md
index 719df12..49756ba 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ const App = () => (
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 { render, hydrate } from 'preact';
import { LocationProvider, ErrorBoundary, Router, lazy, prerender as ssr } from 'preact-iso';
// Asynchronous (throws a promise)
@@ -67,7 +68,10 @@ const App = () => (
);
-hydrate();
+if (typeof window !== 'undefined') {
+ const target = document.getElementById('app');
+ import.meta.env.DEV ? render(, target) : hydrate(, target);
+}
export async function prerender(data) {
return await ssr();
@@ -270,31 +274,6 @@ const App = () => (
);
```
-### `hydrate`
-
-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.
-
-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.
-
-```js
-import { hydrate } from 'preact-iso';
-
-const App = () => (
-
-
Hello World
-
-);
-
-hydrate();
-```
-
-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.
-
### `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.
diff --git a/package.json b/package.json
index 30a1202..509a3c0 100644
--- a/package.json
+++ b/package.json
@@ -7,10 +7,8 @@
"types": "src/index.d.ts",
"exports": {
".": "./src/index.js",
- "./router": "./src/router.js",
- "./lazy": "./src/lazy.js",
"./prerender": "./src/prerender.js",
- "./hydrate": "./src/hydrate.js"
+ "./package.json": "./package.json"
},
"license": "MIT",
"description": "Isomorphic utilities for Preact",
@@ -35,6 +33,11 @@
"preact": ">=10",
"preact-render-to-string": ">=6.4.0"
},
+ "peerDependenciesMeta": {
+ "preact-render-to-string": {
+ "optional": true
+ }
+ },
"devDependencies": {
"@types/mocha": "^10.0.7",
"@types/sinon-chai": "^3.2.12",
diff --git a/src/hydrate.d.ts b/src/hydrate.d.ts
deleted file mode 100644
index 227b5ec..0000000
--- a/src/hydrate.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { ComponentChild, ContainerNode } from 'preact';
-
-export default function hydrate(jsx: ComponentChild, parent?: ContainerNode): void;
diff --git a/src/hydrate.js b/src/hydrate.js
deleted file mode 100644
index 269bc58..0000000
--- a/src/hydrate.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { render, hydrate as hydrativeRender } from 'preact';
-
-let initialized;
-
-/** @type {typeof hydrativeRender} */
-export default function hydrate(jsx, parent) {
- if (typeof window === 'undefined') return;
- let isodata = document.querySelector('script[type=isodata]');
- // @ts-ignore-next
- parent = parent || (isodata && isodata.parentNode) || document.body;
- if (!initialized && isodata) {
- hydrativeRender(jsx, parent);
- } else {
- render(jsx, parent);
- }
- initialized = true;
-}
diff --git a/src/index.d.ts b/src/index.d.ts
index 70ba4a2..2cb7567 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,4 +1,2 @@
-export { default as prerender } from './prerender.js';
+export * from './lazy.js';
export * from './router.js';
-export { default as lazy, ErrorBoundary } from './lazy.js';
-export { default as hydrate } from './hydrate.js';
diff --git a/src/index.js b/src/index.js
index 3bfc751..2cb7567 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,2 @@
-export { Router, LocationProvider, useLocation, Route, useRoute } from './router.js';
-export { default as lazy, ErrorBoundary } from './lazy.js';
-export { default as hydrate } from './hydrate.js';
-
-export function prerender(vnode, options) {
- return import('./prerender.js').then(m => m.default(vnode, options));
-}
+export * from './lazy.js';
+export * from './router.js';
diff --git a/src/lazy.d.ts b/src/lazy.d.ts
index 24e3b1b..7841eef 100644
--- a/src/lazy.d.ts
+++ b/src/lazy.d.ts
@@ -1,6 +1,6 @@
import { ComponentChildren, VNode } from 'preact';
-export default function lazy(load: () => Promise<{ default: T } | T>): T & {
+export function lazy(load: () => Promise<{ default: T } | T>): T & {
preload: () => Promise;
};
diff --git a/src/lazy.js b/src/lazy.js
index 9fb7fe3..a0feaa2 100644
--- a/src/lazy.js
+++ b/src/lazy.js
@@ -10,7 +10,7 @@ options.__b = (vnode) => {
if (oldDiff) oldDiff(vnode);
};
-export default function lazy(load) {
+export function lazy(load) {
let p, c;
const loadModule = () =>
diff --git a/src/prerender.d.ts b/src/prerender.d.ts
index b9958bc..104ba3d 100644
--- a/src/prerender.d.ts
+++ b/src/prerender.d.ts
@@ -9,7 +9,7 @@ export interface PrerenderResult {
links?: Set
}
-export default function prerender(
+export function prerender(
vnode: VNode,
options?: PrerenderOptions
): Promise;
diff --git a/src/prerender.js b/src/prerender.js
index 879576f..9ef2211 100644
--- a/src/prerender.js
+++ b/src/prerender.js
@@ -14,7 +14,7 @@ options.vnode = vnode => {
* @param {object} [options]
* @param {object} [options.props] Additional props to merge into the root JSX element
*/
-export default async function prerender(vnode, options) {
+export async function prerender(vnode, options) {
options = options || {};
const props = options.props;
@@ -25,7 +25,7 @@ export default async function prerender(vnode, options) {
vnode = cloneElement(vnode, props);
}
- let links = new Set();
+ const links = new Set();
vnodeHook = ({ type, props }) => {
if (type === 'a' && props && props.href && (!props.target || props.target === '_self')) {
links.add(props.href);
@@ -33,8 +33,7 @@ export default async function prerender(vnode, options) {
};
try {
- let html = await renderToStringAsync(vnode);
- html += ``;
+ const html = await renderToStringAsync(vnode);
return { html, links };
} finally {
vnodeHook = null;
diff --git a/src/router.d.ts b/src/router.d.ts
index f58d550..d142a08 100644
--- a/src/router.d.ts
+++ b/src/router.d.ts
@@ -9,7 +9,7 @@ type NestedArray = Array>;
/**
* Check if a URL path matches against a URL path pattern.
- *
+ *
* Warning: This is an internal API exported only for testing purpose. API could change in future.
* @param url - URL path (e.g. /user/12345)
* @param route - URL pattern (e.g. /user/:id)
@@ -38,18 +38,12 @@ export function Router(props: {
interface LocationHook {
url: string;
path: string;
- query: Record;
+ pathParams: Record;
+ searchParams: Record;
route: (url: string, replace?: boolean) => void;
}
export const useLocation: () => LocationHook;
-interface RouteHook {
- path: string;
- query: Record;
- params: Record;
-}
-export const useRoute: () => RouteHook;
-
type RoutableProps =
| { path: string; default?: false; }
| { path?: never; default: true; }
diff --git a/src/router.js b/src/router.js
index ac5d5cc..90290cc 100644
--- a/src/router.js
+++ b/src/router.js
@@ -51,12 +51,12 @@ const UPDATE = (state, url) => {
export const exec = (url, route, matches = {}) => {
url = url.split('/').filter(Boolean);
route = (route || '').split('/').filter(Boolean);
- if (!matches.params) matches.params = {};
+ if (!matches.pathParams) matches.pathParams = {};
for (let i = 0, val, rest; i < Math.max(url.length, route.length); i++) {
- let [, m, param, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
+ let [, m, pathParam, flag] = (route[i] || '').match(/^(:?)(.*?)([+*?]?)$/);
val = url[i];
// segment match:
- if (!m && param == val) continue;
+ if (!m && pathParam == val) continue;
// /foo/* match
if (!m && val && flag == '*') {
matches.rest = '/' + url.slice(i).map(decodeURIComponent).join('/');
@@ -69,8 +69,8 @@ export const exec = (url, route, matches = {}) => {
if (rest) val = url.slice(i).map(decodeURIComponent).join('/') || undefined;
// normal/optional field:
else if (val) val = decodeURIComponent(val);
- matches.params[param] = val;
- if (!(param in matches)) matches[param] = val;
+ matches.pathParams[pathParam] = val;
+ if (!(pathParam in matches)) matches[pathParam] = val;
if (rest) break;
}
return matches;
@@ -80,19 +80,19 @@ export const exec = (url, route, matches = {}) => {
* @type {import('./router.d.ts').LocationProvider}
*/
export function LocationProvider(props) {
- // @ts-expect-error - props.url is not implemented correctly & will be removed in the future
- const [url, route] = useReducer(UPDATE, props.url || location.pathname + location.search);
+ const [url, route] = useReducer(UPDATE, location.pathname + location.search);
if (props.scope) scope = props.scope;
const wasPush = push === true;
const value = useMemo(() => {
const u = new URL(url, location.origin);
const path = u.pathname.replace(/\/+$/g, '') || '/';
- // @ts-ignore-next
+
return {
url,
path,
- query: Object.fromEntries(u.searchParams),
+ pathParams: {},
+ searchParams: Object.fromEntries(u.searchParams),
route: (url, replace) => route({ url, replace }),
wasPush
};
@@ -108,7 +108,6 @@ export function LocationProvider(props) {
};
}, []);
- // @ts-ignore
return h(LocationProvider.ctx.Provider, { value }, props.children);
}
@@ -117,8 +116,8 @@ const RESOLVED = Promise.resolve();
export function Router(props) {
const [c, update] = useReducer(c => c + 1, 0);
- const { url, query, wasPush, path } = useLocation();
- const { rest = path, params = {} } = useContext(RouteContext);
+ const { url, path, pathParams, searchParams, wasPush } = useLocation();
+ const { rest = path } = useContext(RouterContext);
const isLoading = useRef(false);
const prevRoute = useRef(path);
@@ -138,7 +137,7 @@ export function Router(props) {
let pathRoute, defaultRoute, matchProps;
toChildArray(props.children).some((/** @type {VNode} */ vnode) => {
- const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, query, params, rest: '' }));
+ const matches = exec(rest, vnode.props.path, (matchProps = { ...vnode.props, path: rest, pathParams, searchParams }));
if (matches) return (pathRoute = cloneElement(vnode, matchProps));
if (vnode.props.default) defaultRoute = cloneElement(vnode, matchProps);
});
@@ -151,7 +150,7 @@ export function Router(props) {
const routeChanged = useMemo(() => {
prev.current = cur.current;
- cur.current = /** @type {VNode} */ (h(RouteContext.Provider, { value: matchProps }, incoming));
+ cur.current = /** @type {VNode} */ (h(RouterContext.Provider, { value: matchProps }, incoming));
// Only mark as an update if the route component changed.
const outgoing = prev.current && prev.current.props.children;
@@ -265,11 +264,10 @@ Router.Provider = LocationProvider;
LocationProvider.ctx = createContext(
/** @type {import('./router.d.ts').LocationHook & { wasPush: boolean }} */ ({})
);
-const RouteContext = createContext(
- /** @type {import('./router.d.ts').RouteHook & { rest: string }} */ ({})
+const RouterContext = createContext(
+ /** @type {{ rest: string }} */ ({})
);
export const Route = props => h(props.component, props);
export const useLocation = () => useContext(LocationProvider.ctx);
-export const useRoute = () => useContext(RouteContext);
diff --git a/test/lazy.test.js b/test/lazy.test.js
index 1d29f57..6af6f80 100644
--- a/test/lazy.test.js
+++ b/test/lazy.test.js
@@ -4,7 +4,7 @@ import * as sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { LocationProvider, Router } from '../src/router.js';
-import lazy, { ErrorBoundary } from '../src/lazy.js';
+import { lazy, ErrorBoundary } from '../src/lazy.js';
import './setup.js';
diff --git a/test/node/prerender.test.js b/test/node/prerender.test.js
index 434b151..ba771e7 100644
--- a/test/node/prerender.test.js
+++ b/test/node/prerender.test.js
@@ -2,7 +2,7 @@ import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { html } from 'htm/preact';
-import { default as prerender } from '../../src/prerender.js';
+import { prerender } from '../../src/prerender.js';
test('extracts links', async () => {
const App = () => html`
@@ -20,10 +20,4 @@ test('extracts links', async () => {
assert.ok(links.has('/baz'), `missing: /baz`);
});
-test('appends iso data script', async () => {
- const { html: h } = await prerender(html``);
- // Empty for now, but used for hydration vs render detection
- assert.match(h, /