sobria
ページの一覧

読み込みと値の受け渡し

要素は、JS が届くまでサーバーで描画した HTML のまま表示されます。要素の JS を読み込むと要素が起動し、操作できるようになります。

要素を起動するタイミング

デフォルトでは、ページを読み込むとすぐに要素の JS を読み込み、要素を起動します。起動のタイミングは、要素のファイルと、要素を配置する箇所で変更できます。

要素のファイルで指定する

要素のファイルで load を export すると、その要素のデフォルトのタイミングを変更できます。

// src/elements/chart.tsx
export const load = "visible";

export default function Chart(props: { values: number[] }) {
  return <p>{props.values.join(", ")}</p>;
}

この例の要素は、初めて画面に入った時点で起動します。記述できる値と、それぞれの起動のタイミングは、load:* に記述しています。

配置する箇所で指定する

サーバーコンポーネントで要素を配置するときに、load:* の属性でタイミングを指定できます。この指定は、要素のファイルの export const load より優先されます。

// src/routes/page.tsx
import Chart from "../elements/chart.tsx";

export default function Page() {
  return (
    <>
      <Chart load:load values={[1, 2, 3]} />
      <Chart load:idle values={[4, 5, 6]} />
      <Chart load:media="(min-width: 800px)" values={[7, 8, 9]} />
    </>
  );
}

load:media を指定した要素は、メディアクエリに一致した時点で起動します。最初から一致していれば、すぐに起動します。メディアクエリは配置する箇所でだけ指定でき、export const load には記述できません。

同じ要素を複数配置したときは、最初に条件を満たした要素の時点で JS を読み込みます。ほかの要素は、それぞれの条件を満たした時点で起動します。

起動する前の要素

起動する前の要素は、サーバーで描画した内容と CSS で表示されます。イベントのハンドラーは動作せず、ctx.expose で公開したメソッドもまだ要素にありません。外部から公開したメソッドを呼び出すときの注意は、外部からの利用 で説明します。

要素は、起動するときに、サーバーで描画した Shadow DOM のノードをそのまま使用します。そのため、起動する前に入力欄に入力した文字とフォーカスは、起動した後も残ります。ただし、起動する前に入力した文字は、入力欄の value に渡した signal に反映されません。signal は、onInput などのハンドラーが更新するまで、サーバーで描画したときの値のままです。signal が別の値に変化すると、入力欄の文字は signal の値で上書きされます。

サーバーとクライアントで描画した内容が異なると、要素は Shadow DOM の内容を作り直し、開発時はコンソールに警告を出力します。作り直した要素では、入力欄の文字はサーバーで描画した値に戻り、フォーカスも外れます。

要素の中で使用した要素

要素の Shadow DOM の中で使用した要素は、親の要素と一緒に JS を読み込み、親と同時に起動します。子の要素のファイルの export const load は適用されません。要素の中で load:* を記述すると、型エラーになります。

サーバーコンポーネントが要素の子として配置した要素は、その要素自身のタイミングで起動します。

サーバーで取得した値を引き継ぐ

要素の関数は、サーバーでも実行します。要素の中で非同期の memo を使用すると、サーバーで解決した値がページに埋め込まれ、要素はその値を使用して起動します。

// src/lib/users.ts
import { defineServerFn } from "sobria/server";
import { z } from "zod";

import { db } from "./db.ts";

export const getUser = defineServerFn({ method: "GET", input: z.number() })(async (_c, { input }) =>
  db.users.find(input),
);
// src/elements/user-card.tsx
import { memo } from "sobria";

import { getUser } from "../lib/users.ts";

export default function UserCard(props: { userId: number }) {
  const user = memo(() => getUser(props.userId));
  return <p>{user().name}</p>;
}

サーバーで描画するときは、getUser をサーバーで直接実行します。ブラウザで要素が起動するときは、埋め込まれた値を使用するので、getUser でサーバーに再び通信しません。要素が起動しても、表示はサーバーで描画した内容と同じです。

同期の memo の値は引き継がず、ブラウザで計算し直します。

memo の関数が async iterable を返すとき、または async iterable に解決する Promise を返すときは、サーバーは最初の値で描画し、最初の値をページに埋め込んで、async iterable の反復を終了します。ブラウザで要素が起動すると、埋め込まれた値を表示したまま memo の関数を改めて実行し、async iterable の値が届くたびに、memo の値をその値に置換します。

値を引き継がない memo

次の memo は、ブラウザで値を取得し直します。

  • 要素が返す JSX の中の関数({() => …})で作成した memo と、要素の JSX に配置した関数のコンポーネントで作成した memo。値を引き継ぐには、要素の関数を実行している間に memo を作成します
  • サーバーで失敗した memo
  • 値が、サーバーコンポーネントから渡す値 に列挙した、渡せない値である memo。サーバーは、その値をエラーとして記録します

サーバーから要素に渡す props

サーバーコンポーネントから要素には、文字列や数値のほかに、Date や Set のようなオブジェクトと、サーバー関数を渡せます。オブジェクトはページに埋め込み、型を保持したまま要素に渡します。渡せる値と、渡すとエラーになる値は、サーバーコンポーネントから渡す値 に記述しています。

// src/elements/event-form.tsx
import { signal } from "sobria";

type Props = {
  heading: string;
  at: Date;
  tags: Set<string>;
  onSave: (heading: string) => Promise<string>;
};

export default function EventForm(props: Props) {
  const [result, setResult] = signal("");
  return (
    <div>
      <p>
        {props.heading} {props.at.toLocaleString()} {[...props.tags].join("、")}
      </p>
      <button onClick={async () => setResult(await props.onSave(props.heading))}>保存</button>
      <output>{result()}</output>
    </div>
  );
}
// src/routes/page.tsx
import EventForm from "../elements/event-form.tsx";
import { saveEvent } from "../lib/events.ts";

export default function Page() {
  return (
    <EventForm
      heading="定例"
      at={new Date("2026-11-02T10:00:00Z")}
      tags={new Set(["週次", "全員"])}
      onSave={saveEvent}
    />
  );
}

この例の saveEvent はサーバー関数です。onSave に、async (heading) => heading のようなサーバー関数でない関数を渡すと、サーバーで描画するときにエラーになります。