Using preact

Frugal comes with optional integration with Preact. You can use it on the server or at build time (as a template engine) or on the client through island of interactivity

Preact integration rely on you providing the Preact version you want via an import map:

{
    "imports": {
        "preact": "https://esm.sh/preact@10.11.2",
        "preact/jsx-runtime": "https://esm.sh/preact@10.11.2/jsx-runtime",
        "preact/hooks": "https://esm.sh/preact@10.11.2/hooks",
        "preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.5?deps=preact@10.11.2"
    }
}

This means you provide the version of Preact that suits you, and Frugal will use it.

Server-side Preact or at build time

To use Preact at build time or on the server, you only need to use the getContentFrom function in your page descriptor:

import { getContentFrom } from 'https://deno.land/x/frugal/preact.server.ts';
import { Page } from './Page.tsx';

//...

export const getContent = getContentFrom(Page);

The getContentFrom will return a getContent function of a page descriptor from a Preact component (here the Page component).

The data object returned by your data fetching methods (getStaticData, getDynamicData and handlers) will be embedded as JSON in the generated markup for islands. This means that the data object needs to be serializable.

The Page component will receive in its props the loaderContext for you to inject any style or script loaded by Frugal:

import { Head, PageProps } from 'https://deno.land/x/frugal/preact.server.ts';

export function Page({ loaderContext }: PageProps) {
    const bodyBundleUrl = loaderContext.get('script')[descriptor].['body'];
    const styleUrl = loaderContext.get('style');

    return <>
        <Head>
            <link rel='stylesheet' href={styleUrl} />
        </Head>
        {/* ... */}
        <script async type='module' src={bodyBundleUrl}></script>
    </>
}

The <Head> component allows you to set what's in the <head> of your page from everywhere in your component tree.

useData and usePathname hooks

Integration with Preact comes with two hooks useData and usePathname that will return the current data object and the current pathname:

import {
    useData,
    usePathname,
} from 'https://deno.land/x/frugal/preact.client.ts';
import { type Data } from '../types.ts';

export function MyComponent() {
    const data = useData<Data>();
    const pathname = usePathname();
}

Those hooks work both on the server side (inside standard components) and on the client side (inside islands).

Client-side Preact with islands

First, you need to create an island version of your component (by convention, use the .island.tsx suffix):

/* @jsxRuntime automatic */
/* @jsxImportSource preact */
import { Island } from 'https://deno.land/x/frugal/preact.client.ts';

import { MyComponent, MyComponentProps } from './MyComponent.tsx';
import { NAME } from './MyComponent.script.ts';

export function MyComponentIsland(props: MyComponentProps) {
    return <Island props={props} Component={MyComponent} name={NAME} />;
}

The props object passed to the <Island> component will be embedded as JSON in the generated markup for islands. This means that the props object needs to be serializable.

Since the data object for the page is also serialized and injected in the html markup avoid passing a props object to the island if you could use useData instead. This will keep the html markup of the page light.

Defining the island is not enough, we need to hydrate it client side. Since it is a client-side action, we need to use a script module:

You need to create a script module (the ./MyComponent.script.ts module in the previous code block, a module matching the script loader pattern) that hydrate your component:

import { MyComponent } from './MyComponent.tsx';
import { hydrate } from 'https://deno.land/x/frugal/preact.client.ts';

export const NAME = 'MyComponentIsland';

export function main() {
    hydrate(NAME, () => MyComponent);
}

The NAME export is the unique identifier for your component. It will be used by the <Island> component to uniquely identify the generated DOM node as "hydratable with the component MyComponent". The hydrate function will use this name to query all DOM nodes that need to be hydrated with MyComponent.

The hydrate function takes as parameter a function returning the component () => MyComponent, and not directly the component. This is done to work with hydration-strategy