Preact runtime

Frugal comes with an optional integration with Preact. You can write your static markup with JSX and declare island for stateful components that need to be hydrated client-side.

Configuration

First, you'll need an Import Map. Frugal uses bare specifiers internally to avoid locking you with a specific version of peer dependencies: import * as preact from 'preact'. Those kinds of imports need to be "mapped" to actual URLs where you can choose the specific version you wish to use.

import_map.json
{
    "imports": {
        "preact": "https://esm.sh/preact@10.13.1",
        "preact/": "https://esm.sh/preact@10.13.1/",
        "preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.6?external=preact"
    }
}
1
2
3
4
5
6
7

You'll also need a deno.json config file to configure the JSX and the Import Map :

deno.json
{
    "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "preact"
    },
    "importMap": "./import_map.json"
}
1
2
3
4
5
6
7

Now that Deno is configured to understand jsx correctly, we need to configure Frugal :

frugal.config.ts
import { Config } from "https://deno.land/x/frugal@0.9.6/mod.ts"

export default {
    ...
    esbuild: {
        ...
        jsx: "automatic",
        jsxImportSource: "preact",
    },
    ...
} satisfies Config;
1
2
3
4
5
6
7
8
9
10
11

Now frugal is ready to process JSX using Preact.

Server-only runtime

Those functions can only be used in server-side components. They can't be used inside an island.

getRenderFrom

The only thing that changes in page descriptors is the render function. Instead of defining it ourselves, the preact runtime computes a render function for us from a JSX component :

page.tsx
import { getRenderFrom } from "https://deno.land/x/frugal@0.9.6/runtime/preact.server.ts"

export const render = getRenderFrom(App)

function App() {
    return <h1>Hello World !</h1>
}
1
2
3
4
5
6
7

Parameters

The getRenderFrom takes two parameters, the root JSX component and an optional config object RenderConfig

type RenderConfig = {
    Document?: preact.ComponentType<DocumentProps>;
    embedData?: boolean;
};

export type DocumentProps = {
    head: preact.VNode[];
    descriptor: string;
    assets: descriptor.Assets;
    dangerouslySetInnerHTML: { __html: string };
};
1
2
3
4
5
6
7
8
9
10
11
Document

The root JSX component only describes markup inside the body. To modify the rest of the document, you can pass a JSX Component that takes DocumentProps.

Tip

If you want to modify the <head> of the document, use the <Head> component instead.

embedData

By default, Frugal outputs static pages without any client-side script. But if you have client-side island, you might need access to the data object used to render the page server-side. The embedData parameter instructs Frugal to embed the data object in an inline script for you to access via useData

Warning

You will get an error if you call the hook useData inside an island with embedData: false. You must have embedData: true for the hook to work client-side.

Client-safe runtime

Every components, hooks, or method described here are usable inside server components or island. You can use them everywhere.

<Island>

Wrapping your stateful client-side component in the <Island> component will create an island. The <Island> component will output all the necessary markup to hydrate the component client-side.

MyComponentIsland.tsx
import { Island } from "https://deno.land/x/frugal@0.9.6/runtime/preact.client.ts"
import { MyComponent, MyComponentProps } from './MyComponent.tsx'
import { NAME } from "./MyComponentIsland.script.ts"

function MyComponentIsland(props: MyComponentProps) {
    return <Island Component={MyComponent} props={props} name={NAME} />
}
1
2
3
4
5
6
7
Warning

The <Island> component does not perform any hydration; it only generates the markup necessary for hydration. The hydration is done via a client-side call to the hydrate function.

The component accepts the following props :

export type IslandProps<PROPS> = {
    strategy?: HydrationStrategy;
    clientOnly?: boolean;
    query?: string;
    name: string;
    Component: preact.ComponentType<PROPS>;
    props: preact.RenderableProps<PROPS>;
};

export type HydrationStrategy = "load" | "idle" | "visible" | "media-query" | "never";
1
2
3
4
5
6
7
8
9
10

strategy

This prop selects the hydration strategy for the island :

  • "load" will hydrate the island on page load (default behavior)
  • "idle" will defer hydration until the browser is idle (via requestIdleCallback or setTimeout for browsers not supporting it)
  • "visible" will defer hydration until the island enters the viewport (via InsersectionObserver)
  • "media-query" will hydrate the island if the viewport matches a given media query on load
  • "never" will turn off hydration for this island

clientOnly

This prop will disable SSR for the island. No markup will be generated in the HTML, and the component will only render client-side (for browsers able to process the javascript)

query

The media query to match if you chose the "media-query" strategy.

name

A unique name for your island. This name will be used as a selector to find the DOM node to hydrate.

Component

Your stateful client-side component inside the island.

props

The props passed to your component.

Warning

The props of your component will be serialized and embedded in the HTML markup, so the props must be serializable

Moreover, if you have multiple instances of the same island on the page and the props are derived from the page data object, it might be more efficient to have an island without props and to use useData with embedData:true to compute the props from the data object. Instead of having a JSON of the props embedded for each instance of your island, you'll have a single JSON of the data object embedded. You'll have to decide what option is the best fit for your case.

hydrate

This is the function to call client-side (inside a script) to hydrate an <Island> :

MyComponentIsland.script.ts
import { hydrate } from "https://deno.land/x/frugal@0.9.6/runtime/preact.client.ts"
import { MyComponent } from "./MyComponent.tsx"

export const NAME = "MyComponent";

if (import.meta.environment === 'client') {
    hydrate(NAME, () => MyComponent)
}
1
2
3
4
5
6
7
8

This function will find every island with a matching name and hydrate them with the component MyComponent.

Parameters

The hydrate function takes two parameters. The name of the island to hydrate and a function (sync or async) returning a component.

Dynamic import and deferred hydration

The second parameter of the hydrate function can be an async callback to enable dynamic loading of components with code-splitting. Since the hydrate function calls the callback parameter only during actual hydration, for Islands with deferred hydration ("idle", "visible", "media-query") you can use this pattern with code splitting enabled (via esbuild config):

MyComponentIsland.script.ts
import { hydrate } from "https://deno.land/x/frugal@0.9.6/runtime/preact.client.ts"

export const NAME = "MyComponent";

if (import.meta.environment === 'client') {
    hydrate(NAME, () => (await import('./MyComponent.tsx')).MyComponent)
}
1
2
3
4
5
6
7

The JS chunk containing your component will not be loaded immediately. Instead, it will be deferred until the island is hydrated.

<Head>

This component allows you to change the <head> content and the attributes of <html> and <body> from everywhere in your JSX :

Component.tsx
import { Head } from "https://deno.land/x/frugal@0.9.6/runtime/preact.client.ts"

function Component() {
    return <>
        <Head>
            {/* any tag inside the <head> */}
            <title>Title of my Page</title>
            {/* html tag attributes */}
            <html lang="en" amp />
            {/* body tag attributes */}
            <body class="root" />
        </Head>
    </>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

useData

This hook gives you access to the data object of the page.

Warning

For this hook to work client-side, you need embedData:true in the getRenderFrom function.

usePathname

This hook gives you access to the current pathname (the route of the page, compiled with the current path object).