Deno Fresh のアイランド

作成日
更新日

Fresh のアイランド

インタラクティブアイランド

アイランドは、Freshにおけるクライアントサイドのインタラクティブ性を可能にします。アイランドは、サーバー上でレンダリングされ、クライアント上でハイドレーションされる孤立したPreactコンポーネントです。これは、通常サーバ上でのみレンダリングされるため、Freshの他のすべてのコンポーネントとは異なります。

アイランドは、Freshプロジェクトのislands/フォルダにファイルを作成することで定義されます。このファイルの名前は、PascalCaseまたはkebab-caseの島の名前でなければなりません。

islands/my-island.tsx
import { useSignal } from "@preact/signals";

export default function MyIsland() {
  const count = useSignal(0);

  return (
    <div>
      Counter is at {count}.{" "}
      <button onClick={() => (count.value += 1)}>+</button>
    </div>
  );
}

アイランドは、通常のPreactコンポーネントのようにページ内で使用することができます。Freshは、クライアント上で自動的にアイランドを再ハイドレーティングします。

ハイドレーションとは 静的なウェブページを動的なものに変換すること。

import MyIsland from "../islands/my-island.tsx";

export default function Home() {
  return <MyIsland />;
}

JSXをアイランドに渡す方法

アイランドでは、childrenプロパティを介してJSX要素を渡すことがサポートされています。

islands/my-island.tsx
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";

interface Props {
  children: ComponentChildren;
}

export default function MyIsland({ children }: Props) {
  const count = useSignal(0);

  return (
    <div>
      カウンターの値は {count}.{" "}
      <button onClick={() => (count.value += 1)}>+</button>
      {children}
    </div>
  );
}

これにより、サーバーでレンダリングされた静的なコンテンツをブラウザ上のアイランドに渡すことができます。

routes/my-island.tsx
import MyIsland from "../islands/my-island.tsx";

export default function Home() {
  return (
    <MyIsland>
      <p>このテキストはサーバーでレンダリングされています</p>
    </MyIsland>
  );
}

また、components/ディレクトリに共有コンポーネントを作成し、静的コンテンツやインタラクティブなアイランドの両方で使用できます。これらのコンポーネントがアイランド内で使用されると、onClickハンドラーのようなインタラクティブ性を追加できます(アイランドの外でonClickハンドラーを使うと、動作しません)。

islands/my-island.tsx
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";
import Card from "../components/Card.tsx";
import { Button } from "../components/Button.tsx";

interface Props {
  children: ComponentChildren;
}

export default function MyIsland({ children }: Props) {
  const count = useSignal(0);

  return (
    <Card>
      カウンターの値は {count}.{" "}
      <Button onClick={() => (count.value += 1)}>+</Button>
      {children}
    </Card>
  );
}
components/Card.tsx
import { ComponentChildren } from "preact";

interface CardProps {
  children: ComponentChildren;
}

export default function Card({ children }: CardProps) {
  return (
    <div style={cardStyle}>
      {children}
    </div>
  );
}

const cardStyle = {
  padding: "16px",
  border: "1px solid #ddd",
  borderRadius: "8px",
  boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
  backgroundColor: "#fff",
};

アイランドに他のプロパティを渡す

アイランドにプロパティを渡すことはサポートされていますが、プロパティはシリアライズ可能なものである必要があります。Freshでは次のタイプの値をシリアライズできます:

  • プリミティブ型(string、boolean、bigint、null)
  • ほとんどの数値(Infinity、-Infinity、NaNは自動的にnullに変換)
  • 文字列キーとシリアライズ可能な値を持つプレーンオブジェクト
  • シリアライズ可能な値を含む配列
  • Uint8Array
  • JSX要素(props.childrenに制限)
  • Preact Signals(内部の値がシリアライズ可能である場合)

循環参照もサポートされています。オブジェクトやシグナルが複数回参照される場合、それは一度だけシリアライズされ、逆シリアル化時に参照が復元されます。Dateやカスタムクラス、関数などの複雑なオブジェクトはサポートされていません。

サーバーサイドレンダリング時には、FreshはHTMLに特別なコメントを付けて各アイランドの配置位置を示します。これにより、クライアント側に送信されるコードがインタラクティブなアイランドの静的な子要素のためにハイドレーションを必要とせず、アイランドを正しい場所に配置するために必要な情報を持つことができます。インタラクティブ性が必要ない場合、JavaScriptはクライアントに送信されません。

<!--frsh-myisland_default:default:0-->
<div>
  カウンターの値は 0.
  <button>+</button>
  <!--frsh-slot-myisland_default:children-->
  <p>このテキストはサーバーでレンダリングされています</p>
  <!--/frsh-slot-myisland_default:children-->
</div>
<!--/frsh-myisland_default:default:0-->

アイランドのネスト

アイランドは他のアイランド内にネストすることもできます。この場合、通常のPreactコンポーネントとして動作しますが、シリアライズされたプロパティが存在する場合、それらを受け取ります。

islands/other-islands.tsx
import { useSignal } from "@preact/signals";
import { ComponentChildren } from "preact";

interface Props {
  children: ComponentChildren;
  foo: string;
}

function randomNumber() {
  return Math.floor(Math.random() * 100);
}

export default function MyIsland({ children, foo }: Props) {
  const number = useSignal(randomNumber());

  return (
    <div>
      <p>プロパティからの文字列: {foo}</p>
      <p>
        <button onClick={() => (number.value = randomNumber())}>ランダム</button>
        数値は: {number}.
      </p>
    </div>
  );
}

つまり、Freshでは、アプリに最適な方法で静的な部分とインタラクティブな部分を組み合わせることができます。必要なJavaScriptだけがブラウザに送信されます。

クライアントのみでアイランドをレンダリング

EventSourcenavigator.getUserMediaなどのクライアント専用APIを使用すると、このコンポーネントはサーバーで実行されず、次のようなエラーが発生します:

ルート処理またはページレンダリング中にエラーが発生しました。ReferenceError: EventSource is not defined at Object.MyIsland (file:///Users/someuser/fresh-project/islandsmy-island.tsx:6:18) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2602) at m (https://esm.sh/v129/preact-render-to-string@6.2.0/X-ZS8q/denonext/preact-render-to-string.mjs:2:2113) ....

この問題を解決するには、IS_BROWSERフラグをガードとして使用します

island/my-island.tsx
import { IS_BROWSER } from "$fresh/runtime.ts";

export function MyIsland() {
  // アイランドに適した事前レンダリング可能なJSXを返す
  if (!IS_BROWSER) return <div></div>;

  // ブラウザで実行する必要があるコードをここに記述
  // 例:EventSource, navigator.getUserMedia など
  return <div></div>;
}
サイトアイコン
公開日
更新日