Resources on the server
useResource reads from a resource cache the renderer provides.
useResource reads from a resource cache the renderer provides. A resource has a key: the explicit
key option (stored as k:<key>), otherwise the scope id plus r plus its creation index in that scope
(0-0-r0 below). Use explicit keys for anything that matters; automatic keys change if you reorder
components.
renderToStringAsync#
renderToStringAsync(code, { renderId?, timeoutMs?, maxPasses? }) renders in passes. Each pass renders
with the values found so far; fetches the pass started are awaited; if any produced a new value, the next
pass runs, and a resource that was pending in pass N is ready in pass N+1. A pass that learns nothing new
is final. It resolves to { html, data }, where data maps resource keys to the resolved values the
final pass asked for.
import { useResource } from "@shadow-js/core";
import { Suspense } from "@shadow-js/web";
import { renderToString, renderToStringAsync } from "@shadow-js/web/server";
function Name(props: { load: () => Promise<string> }) {
const name = useResource(() => props.load(), { key: "name" });
return <strong>{name()}</strong>;
}
function App(props: { load: () => Promise<string> }) {
return (
<Suspense fallback={<em>loading</em>}>
<Name load={props.load} />
</Suspense>
);
}
const load = () => Promise.resolve("Ada");
// Synchronous: the fallback.
console.log(renderToString(() => <App load={load} />).replace(/<[^>]+>/g, ""));
// One pass is not enough for one resource: pass 1 starts the fetch, pass 2 renders with its value.
try {
await renderToStringAsync(() => <App load={load} />, { maxPasses: 1 });
} catch (error) {
console.log(String(error));
}
const { html, data } = await renderToStringAsync(() => <App load={load} />);
console.log(html);
console.log(JSON.stringify(data));
try {
await renderToStringAsync(() => <App load={() => new Promise<string>(() => undefined)} />, {
timeoutMs: 5,
});
} catch (error) {
console.log(String(error));
}
// Without a key the resource is named after its scope; a rejected fetch is not cached.
function Anonymous() {
const value = useResource(() => Promise.resolve(1));
return <i>{value()}</i>;
}
console.log(JSON.stringify((await renderToStringAsync(() => <Anonymous />)).data));
const failed = await renderToStringAsync(() => <App load={() => Promise.reject(new Error("down"))} />);
console.log(failed.html.replace(/<[^>]+>/g, ""), JSON.stringify(failed.data));
loading
SsrError: [shadow SSR_BOUNDARY_WATERFALL] Resources kept loading new resources after 1 passes; raise maxPasses or flatten the data flow.
<strong data-hk=Facts to rely on:
maxPassesdefaults to 10 andtimeoutMsto 10 000. A tree with k levels of dependent resources (a resource that is only created after another one resolved) needs k + 1 passes. Exceeding the budget rejects withSsrErrorcodeSSR_BOUNDARY_WATERFALL; running out of time rejects withSSR_TIMEOUTand abandons the remaining work. Fix a waterfall by starting the fetches together, not by raising the limit.- A resource that rejects is not cached; its boundary renders the fallback and the client fetches it itself.
- A fetch whose key already has a value from an earlier pass is not repeated.
- Resources without a key are matched by creation order, so the client must create them in the same order.
Handing the data to the browser#
data goes into the page as JSON and comes back as the data option of hydrate. A resource whose key is
in data resolves from it on its first load without calling the fetcher:
import { serialize } from "@shadow-js/web/server";
// A resource value that tries to close the script element it is embedded in.
const data = { "k:name": "</script>" };
console.log(`<script type="application/json" id="shadow-data">${serialize(data)}</script>`);
<script type=Server data is used once per key: the first load of that resource. A later refetch() or a change of
its source calls the fetcher as usual. renderId in the hydrate options must equal the server's, or no
key matches.