zfb
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

コンポーネントと JSX

HTML と SVG の表記、明示的なリスナー、信頼できる raw HTML を使って、不活性な JSX の記述を作ります。

JSX は記述を作る

自動 JSX ヘルパーは不活性な記述を作ります。記述を作るだけではコンポーネントを呼び出さず、DOM に触れず、記述にレンダリング状態も保存しません。レンダラーがインスタンスごとに関数コンポーネントを一度呼び、その結果からページや島を組み立てます。

記述は type、props、key とランタイムの記述ブランドを公開します。ファクトリは呼び出し元のオブジェクトを変更せず、props をコピーします。記述は通常のオブジェクトとして再利用やコピーができます。JSX を使わずに作るには公開ヘルパー h を使います。isDescription はブランドを検査し、flattenChildren はランタイムと同じ子の正規化を適用します。

h では可変長引数の子が props.children より優先されます。可変長引数の子を渡さなければ props.children を使います。JSX ファクトリでは明示的な key 引数が props.key より優先され、key がなければ null に正規化されます。

子として使える値

子には文字列、有限数、記述、スカラー値を持つリアクティブな signal、およびそれらの配列を使えます。入れ子の配列は平坦化されます。null、undefined、真偽値は省略されますが、空文字列は残ります。関数、Promise、symbol、bigint、非有限数、任意のオブジェクトはエラーです。

コンポーネントは単一の記述、配列、文字列、数値、または空の値を返せます。結果は同期的です。レンダリングするツリー内に Promise が入るとエラーになり、ランタイムは入れ子のコンポーネントを中断して待つことはありません。zfb は最上位のページ関数の結果をレンダリングする前に待機できます。

コンポーネントが返す Fragment と配列は、可視の要素を増やさずに兄弟のまとまりを保持します。通常の組み込み要素の子配列は単なる走査リストです。記述をコピーするだけで新しいコンポーネントインスタンスが作られることはありません。

テーブルと select の子要素

table と select の構造規則は、静的描画、ハイドレーション、新規マウントで共通です。静的描画では、table の直下に置けるのは caption、colgroup、thead、tbody、tfoot です。table の直下に tr を置くと ZR_PARSER_CONTEXT で失敗します。raw HTML の <table><tr> に対して HTML パーサーが <tbody> を補う場合でも、<tbody> を明示してください。サーバー出力とハイドレーションで採用する DOM の構造を一致させられます。tbody、thead、tfoot には tr、tr には td または th、select には option または optgroup、optgroup には option が必要です。<Row /> のようなコンポーネント、Fragment、For の領域は、これらの位置の直接の子にはできません。option の内容には静的なスカラー文字列だけを使えます。静的描画では、不正な子の位置を ZR_PARSER_CONTEXT で報告します。

構造上の子は、組み込み要素の記述を返す通常の関数を呼び出して作ります。rowElement(row) が tr を返す形は使えますが、<Row /> を tbody の子にする形は使えません。select の直下に置く option や optgroup にも同じ方法を使います。リアクティブなテーブル行については条件分岐とリストを参照してください。

function rowElement(row: { name: string }) {
  return <tr><td>{row.name}</td></tr>;
}

function ResultsTable({ rows }: { rows: { name: string }[] }) {
  return (
    <table>
      <tbody>{rows.map((row) => rowElement(row))}</tbody>
    </table>
  );
}

HTML と SVG の名前を使う

組み込み属性は HTML と SVG の表記に従います。class、for、readonly、tabindex、viewBox を使います。className、htmlFor、charSet、autoComplete、readOnly、tabIndex、onClick、strokeWidth、dangerouslySetInnerHTML などの React 式の表記は検証に失敗し、書き換えもそのままの出力もされません。対応する HTML または SVG の表記がサポートされていれば、エラーにはその名前が示されます(例: use `charset` instead of `charSet` )。onClick には on:click が提示されます。未対応の標準属性は、代替案を示さずにエラーになります。data 属性と ARIA 属性には文字列、数値、真偽値のスカラー値を使えます。ARIA の真偽値は false を含め、対応する文字列として出力されます。

ESLint を使う場合は、ファイル単位に適用する設定を v3 移行ガイドで確認してください。eslint-plugin-react は、これらの HTML 表記を拒否したり自動修正したりすることがあります。

真偽値の HTML 属性は true なら存在し、false なら省略されます。contenteditable などの列挙属性は文字列です。通常の属性では null と undefined は省略されます。サポートされるスカラー属性にはリアクティブな値を使えますが、フォームガイドのフォーム制御の規則に従います。この規則には、静的な readonly={true} または disabled={true} を持つテキスト入力と textarea の表示専用バインディングも含まれます。

hidden 属性は真偽値だけを受け付け、until-found は使えません。inert も真偽値です。ランタイムは非表示用の CSS を追加しないため、表示の挙動はページ側のスタイルで指定します。

ol の start には数値または文字列を指定でき、reversed は真偽値の属性です。

有限の HTML 語彙には、meta[property]、link[as]、integrity、外部 script の script[defer] など、標準的な head とメタデータの属性が含まれます。Open Graph タグ、preload リンク、defer 付き script は通常の JSX として書けるため、rawHtml で囲む必要はありません。属性値の扱いは HTML の分類に従います。真偽値属性は存在の有無で表し、spellcheck などの列挙値は文字列または真偽値、download はオーバーロード属性、ol[start] は数値を受け付けます。未知の標準属性名は引き続きエラーになり、カスタム要素の属性は引き続き文字列のみです。

タグと属性の全一覧、値の分類、オープンエンドの名前パターン、拒否される React 形式の綴りと候補は、対応する HTML と SVG の語彙を参照してください。

SVG 要素は v1 で定められた次の範囲を使えます。svg、g、path、circle、ellipse、rect、line、polyline、polygon、text、tspan、defs、symbol、use、clipPath、mask、linearGradient、radialGradient、stop、title、desc、foreignObject、pattern、filter、marker、image です。対応する SVG 表記には viewBox、preserveAspectRatio、xlink:href、xml:lang、stroke-width、fill-rule、stroke-linecap があります。svg から SVG 名前空間が始まります。foreignObject 内の子は HTML の規則に従い、その中に svg があれば SVG に戻ります。未対応の SVG タグや属性は名前を示してエラーになります。

スタイルとイベント

style prop には CSS 文字列か、対応する CSS 表記のキーを持つプレーンオブジェクトを渡せます。CSSProperties は @takazudo/zfb/zudo-react からエクスポートされています。0 以外の長さは単位を含む文字列で指定し、ランタイムは px を追加しません。長さの数値 0 は使えます。opacity や line-height など単位のないプロパティには数値を、カスタムプロパティには有限の数値または文字列を指定できます。SSR、ハイドレーション、マウント、更新時にも同じ検証を行います。

オブジェクト形式には通常のオブジェクトリテラルを使います。配列、クラスのインスタンス、null プロトタイプのレコード、入れ子のオブジェクト、camelCase のキー、オブジェクト内のリアクティブな値は拒否されます。null と undefined の項目は省略され、項目の順序は保たれます。更新時には次のオブジェクトにないプロパティが削除されます。style prop 全体をリアクティブにできます。

<div
  style={{
    left: "1rem",
    top: 0,
    width: "100%",
    "max-height": "20rem",
    opacity: 0.5,
    "--scale": 2,
    "-webkit-mask-image": "linear-gradient(black, transparent)",
  }}
/>

<iframe title="プレビュー" style={{ width: "100%", height: "480px" }} />

CSS 文字列は、検証対象のオブジェクト形式で表しにくい CSS 構文を使うためのエスケープ手段です。内容はそのまま出力され、対応プロパティ名や値の検証は行われません。

<div style="left: calc(1rem + var(--offset));" />

関数リスナーでは on: の後に大文字小文字を区別するネイティブイベント名を指定します。末尾に :capture を付けることもできます。

<button on:click={save} on:keydown:capture={handleKey}>Save</button>

既知の on:<name> prop では、ハンドラーのイベント型は名前に応じたネイティブ型になります。たとえば on:submit は SubmitEvent、on:keydown は KeyboardEvent です。currentTarget はその prop を持つ要素の型になりますが、target の型は絞り込まれません。未知のイベント名やカスタムイベント名には、単一のイベントを受け取る任意のハンドラーを指定できます。currentTarget が設定されるのはイベント処理中だけです。await の後でも要素が必要なら、その前に保存してください。合成イベントやイベント委譲はありません。ランタイムは所有スコープとともにリスナーを削除します。リスナーが返した Promise が reject された場合、後でスコープが破棄されても報告されます。

onload など小文字の文字列イベント属性は、別形式のインラインハンドラーです。リアクティブな文字列を含む文字列値を受け付け、null と undefined では属性を省略します。レンダラーは文字列をエスケープしてブラウザーのインラインハンドラー属性として出力し、更新します。zudo-react の関数リスナーには変換しません。イベント時にブラウザーがこの文字列を実行することがあるため、信頼できる文字列だけを使ってください。同じイベントにインライン属性と関数リスナーを両方指定しないでください。React 形式の onClick と関数値の onclick は拒否されます。関数リスナーには on:click を使います。

ref は初期値が null の current フィールドを持つ単純なオブジェクトです。ランタイムは有効化コールバックより前にすべての ref を設定します。コールバック ref や ref ファクトリはありません。

iframe のシェル

島の中では、zudo-react は子を持たない <iframe> 要素と、対応する属性、ref、ネイティブリスナーを所有します。src または srcdoc から読み込まれる別文書はブラウザーが所有し、zudo-react はその文書を再調整しません。シェルは子を持たない形にしてください。JSX の子や rawHtml は拒否され、iframe 要素内にパース後のフォールバック DOM がある場合はハイドレーションに失敗します。変更のないハイドレーションでは既存の iframe と文書を引き継ぎます。src または srcdoc を更新すると、ブラウザーがその文書をナビゲートします。

srcdoc はアプリケーションが信頼する入力です。zudo-react は外側の HTML 属性用にエスケープしますが、文書自体はサニタイズしません。アプリケーションが管理する内容を渡し、埋め込み内容に合わせて sandbox と allow を選んでください。

<iframe title="HTML preview" srcdoc={trustedDocument} sandbox="" allow="fullscreen" />

カスタム要素

カスタム要素の名前は小文字で、ハイフンを含む必要があります。属性は文字列で、リアクティブな文字列や null に相当する値も使えます。ネイティブリスナーとランタイムが予約した props に対応します。ランタイムは任意のオブジェクトを要素のプロパティに代入せず、プロパティの型も推測しません。

ブラウザが要素を作るとき、カスタム要素のコンストラクターがブラウザ側のコードを実行する場合があります。後でレンダリングに失敗しても、ランタイムはその外部への影響を元に戻せません。

信頼できる raw HTML

rawHtml は呼び出し元が文字列を信頼できる場合にだけ使ってください。文字列またはリアクティブな文字列を受け取り、children が明示的に空でも併用できません。これはレンダラー用の prop であり、HTML 属性にはなりません。SVG では raw HTML を使えません。

const markup = signal("<em>trusted</em>");
return <section rawHtml={markup} />;

島の中では raw HTML が一つの不透明な領域を占めます。ハイドレーションは境界を確認しますが、内容の検査やバインドはしません。文字列が変わるとその領域の内容だけが置き換わります。マークアップの妥当性、信頼性、追加する外部リソースは呼び出し元の責任です。予約済みの zudo-react コメント形式と入れ子の島ラッパーは拒否されます。クライアントの rawHtml 更新で挿入された script は実行されません。

pre の子をレンダリングした内容が改行で始まる場合、HTML パーサーが先頭の改行を取り除く分を補うため、レンダラーが改行を一つ追加します。rawHtml の内容にはこの補正を行わず、そのまま出力します。

script と style では子を拒否します。インラインの raw-text には rawHtml で静的な文字列を指定し、外部 script は子なしで src を指定できます。リアクティブな内容や内部コメントマーカーには対応しません。現在の要素を閉じる script または style の並びは、大文字小文字を区別せずに拒否されます。また、<!--<script> のように HTML パーサーがレンダラーの終了タグ </script> をテキストとして取り込む script の内容も拒否します。<!--、<!-- <script> -->、通常の <script> というテキストは引き続き受け入れます。この判定は HTML の script トークナイザーのエスケープ状態と二重エスケープ状態に従い、style には RAWTEXT の規則を適用します。拒否時はサーバーでもクライアントでも ZR_RAW_HTML を報告します。ランタイムは任意の JavaScript や CSS をエスケープしません。島の props は実行可能な script ブロックではなく属性で運ばれます。

直接の <script type="application/json"> に JSON データを入れる場合は、JSON に対応したオブジェクトを先にシリアライズし、得られた JSON 内の < と予約済みマーカーの部分文字列をエスケープします。各マーカーの先頭文字を JSON の Unicode エスケープにすると、元の大文字小文字、パース後の値、文字どおりのバックスラッシュ u を保てます。これはデータ転送用であり、サニタイザーでも、実行する JavaScript のシリアライザーでもありません。

function jsonForRawHtml(value: unknown): string {
  const json = JSON.stringify(value);
  if (json === undefined) throw new TypeError("Expected a JSON-compatible object");
  return json
    .replace(/</g, "\\u003c")
    .replace(/data-zfb-island|zr:1:/gi, (marker) =>
      `\\u${marker.charCodeAt(0).toString(16).padStart(4, "0")}${marker.slice(1)}`,
    );
}

const data = { label: 'DATA-zfb-IsLaNd', text: '</script>' };
return <script type="application/json" rawHtml={jsonForRawHtml(data)} />;

script のテキストは JSON のままで、JSON.parse(script.textContent) によって data を正確に復元できます。別の要素の rawHtml に <script type="application/json"> を入れ子にする場合、外側のペイロードには、大文字小文字を区別せず、マーカーを含む JSON の部分文字列を保守的に拒否するフォールバックが適用されます。埋め込む前に同じレシピでエンコードしてください。実際の予約済み属性、プロトコルコメント、レンダラーの境界を取り込むペイロードは引き続き拒否されます。

style 要素のテキストの子は rawHtml に移します。

export function CriticalStyles() {
  const criticalCss = `body { font-family: "Atkinson", sans-serif; }`;
  return <style rawHtml={criticalCss} />;
}

インライン script も同じ形式を使います。外部 script は子を持たない要素として使えます。

export function InlineBootstrap() {
  const bootstrap = "window.appReady = true;";
  return <script rawHtml={bootstrap} />;
}

export function ExternalScript() {
  return <script src="/assets/app.js" />;
}

script と style では、引用符、アンパサンド、< を含め、文字列をそのまま出力します。rawHtml は JavaScript や CSS をエスケープもサニタイズもしません。文字列を信頼し、現在の要素を閉じるタグを含めないでください。この子を持たない規則は script と style に限られます。title と textarea は通常のテキストの子を描画し、一般の要素も子(または子と併用しない rawHtml)を引き続き受け取ります。

関連ページ

Revision History

作成更新