Fresh 1.2 へアップグレード - island の新機能など

| 6 min read
Author: masahiro-kondo masahiro-kondoの画像

Fresh 1.2 リリース

#

Deno の Web フレームワーク Fresh 1.2 がリリースされました。

Fresh 1.2 – welcoming a full-time maintainer, sharing state between islands, limited npm support, and more

Preact のメンテナの人がフルタイムの Fresh メンテナとして Deno にジョインしたことで開発スピードが上がることが期待されています。

1.0 リリース時に当サイトで紹介したのがもう1年近く前になります。

Fresh - Deno の 次世代 Web フレームワーク

ということでフォローアップ的に 1.2 へのアップグレード方法や island の新機能などを見ていきましょう。

プロジェクトの Fresh 1.2 へのアップデート

#

既存のプロジェクトのアップデートは、プロジェクトのルートディレクトリで以下のようにアップデートスクリプトを実行します。

deno run -A -r https://fresh.deno.dev/update .

1.0 のプロジェクトをアップデートすると、deno.json に imports フィールドが追加されて既存の import_map.json は削除されました。

- "importMap": "./import_map.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
+ },
+ "imports": {
+   "$fresh/": "https://deno.land/x/fresh@1.2.0/",
+   "preact": "https://esm.sh/preact@10.15.1",
+   "preact/": "https://esm.sh/preact@10.15.1/",
+   "preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.1.0"
 }
}

これは Deno 本体で deno.json に import map を埋め込むことが可能になったためのようです。

feat: embed import map in the config file by bartlomieju · Pull Request #17478 · denoland/deno

Information

新規にプロジェクトを作成する場合は、以下のように実行します。

deno run -A -r https://fresh.deno.dev

1.2 で新規作成されたプロジェクトでは deno.json の task は以下のように、start に加えて update タスクが追加されています。アップデートしたプロジェクトにも手動でタスクを追加しておきました。これで deno task update でプロジェクトをアップデートできます。

  "tasks": {
    "start": "deno run -A --watch=static/,routes/ dev.ts",
    "update": "deno run -A -r https://fresh.deno.dev/update ."
  },

island での @preact/signals の利用

#

Fresh は Astro や Eleventy などの他の MPA(Multi Page Application) フレームワーク同様、アイランドアーキテクチャーによりインタラクティブな UI を実現可能です。1.2 では island の機能が強化されました。

Passing signals, Uint8Arrays, and circular data in island props

Preact の signals を props として渡せるようになりました。signals は値の変化を検知して自動的に DOM を更新する状態管理のための軽量・高速なライブラリです。

Signals – Preact Guide

プロジェクト作成時に生成される Conter のサンプルページも変わっていました。

1.0 時代の Conter island サンプル

#

1.0 の時の island/Counter.tsx は以下のように useState を使うコードになっていました。

// islands/Counter.tsx
import { useState } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";

interface CounterProps {
  start: number;
}

export default function Counter(props: CounterProps) {
  const [count, setCount] = useState(props.start);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count - 1)} disabled={!IS_BROWSER}>
        -1
      </button>
      <button onClick={() => setCount(count + 1)} disabled={!IS_BROWSER}>
        +1
      </button>
    </div>
  );
}

この island の利用側では、props に数値を渡しています。

import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div>
      <Counter start={3} />
    </div>
  );
}

1.2 の Conter island サンプル

#

1.2 のサンプルページでは island/Counter.tsx から useState がなくなり signal を直接受け取りその値を更新するようになっています。

// islands/Counter.tsx
import type { Signal } from "@preact/signals";
import { Button } from "../components/Button.tsx";

interface CounterProps {
  count: Signal<number>;
}

export default function Counter(props: CounterProps) {
  return (
    <div>
      <p>{props.count}</p>
      <Button onClick={() => props.count.value -= 1}>-1</Button>
      <Button onClick={() => props.count.value += 1}>+1</Button>
    </div>
  );
}
Information

1.0のコードでは ブラウザー外でボタンを非活性にするコードが入っていますが、1.2では Button コンポーネント内で実装されています。

利用側のコードでは、useSignal で数値型の signal を作成して island の props に渡しています。

// routes/index.tsx
import { useSignal } from "@preact/signals";
import Counter from "../islands/Counter.tsx";

export default function Home() {
  const count = useSignal(3);
  return (
    <>
      <div>
        <Counter count={count} />
      </div>
    </>
  );
}

signals はコンポーネントやアプリケーション全体を再レンダリングせず、追跡されている値に関連する部分だけを更新します。このため、アプリケーションのリアクティブ性を実現しつつ、パフォーマンスも確保できるというメリットがあるようです。

Information

React / Preact の従来の方式では Virtual DOM 方式による差分計算で再レンダリングのコストを最小化していました。signals では、Virtual DOM をバイパスして値の変更を直接 DOM 操作に変換することで高速化を実現しているそうです。

ページ内の複数の island に同じ signal を渡して island 間で状態を共有するようなユースケースも可能になります。以下のように3つの Counter のうち2つは一つの signal count1 を共有し、残りの1つは独立した signal count2 を持つようにすると、signal を共有する2つの counter の値は連動します。

// routes/index.tsx
import { useSignal } from "@preact/signals";
import Counter from "../islands/Counter.tsx";

export default function Home() {
  const count1 = useSignal(3);
  const count2 = useSignal(3);
  return (
    <>
      <div>
        <Counter count={count1} />
        <Counter count={count1} />
        <Counter count={count2} />
      </div>
    </>
  );
}

share signals

最後に

#

island は軽量で高速な反応性が要求されるため signals のような方式を導入することには納得感があります。
signals の他 Uint8Arrays、循環データなども props として渡せるようになっています。さらに、island に JSX を渡したり、island をネストさせたりという使い方も可能になっています。
Preact のメンテナがフルタイムメンテナとして加わったことがこのような機能強化につながっているのでしょう。

Fresh は今後も活発な開発が続きそうですので、引き続きウォッチしていきたいと思います。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。