スロット

スロットを使用すると、コンポーネントはコンポーネントの JSX 子を一種の入力として扱い、これらの子をコンポーネントの DOM ツリーに投影できます。

この概念は、フレームワークによって異なる名前で呼ばれています。

  • Angular では、コンテンツプロジェクションと呼ばれています。
  • React では、props の children になります。
  • Web コンポーネントでは、<slot> とも呼ばれます。

これを実現するためのメイン API は、@builder.io/qwik でエクスポートされる <Slot> コンポーネントです。

import { Slot, component$ } from '@builder.io/qwik';
 
const Button = component$(() => {
  return (
    <button>
      Content: <Slot />
    </button>
  );
});
 
export default component$(() => {
  return (
    <Button>
      This goes inside {'<Button>'} component marked by{`<Slot>`}
    </Button>
  );
});

<Slot> コンポーネントは、コンポーネントの子のプレースホルダーです。 <Slot> コンポーネントは、レンダリング中にコンポーネントの子に置き換えられます。

: Qwik のスロットは宣言型であり、Qwik は親と子を分離してレンダリングできます。スロットは宣言型であるため、子はコンポーネントによって読み取ったり、変換したりすることはできません。

Qwik のスロットは、コンテンツの指定されたプレースホルダーのように機能し、コンポーネントが独立した状態を維持し、不要な再レンダリングを回避できるようにします。この設定により、柔軟性が保たれ、管理が容易になります。親の変更によって頻繁かつ複雑な子の更新につながる可能性のある直接的な子の処理とは異なります。スロットは、スムーズで効率的なコンポーネント構造を維持するのに役立ちます。

子に基づいて何らかの作業を行う必要があり、親と子が一緒に遅延ロードされることの欠点が理解されているという特殊な状況では、インラインコンポーネントも別の選択肢となります。

名前付きスロット

Slot コンポーネントは、異なる name プロパティを持っている限り、同じコンポーネント内で複数回使用できます。

import { Slot, component$, useStylesScoped$ } from '@builder.io/qwik';
import CSS from './index.css?inline';
 
const Tab = component$(() => {
  useStylesScoped$(CSS);
  return (
    <section>
      <h2>
        <Slot name="title" />
      </h2>
      <div>
        <Slot /> {/* default slot */}
        <div>
          <Slot name="footer" />
        </div>
      </div>
    </section>
  );
});
 
export default component$(() => {
  return (
    <Tab>
      <div q:slot="title">Qwik</div>
      <div>A resumable framework for building instant web applications</div>
      <span q:slot="footer">made with ❤️ by </span>
      <a q:slot="footer" href="https://builder.io">
        builder.io
      </a>
    </Tab>
  );
});

ここで、<Tab> コンポーネントを使用する場合、子を渡し、q:slot 属性を使用して、配置するスロットを指定できます。

次の点に注意してください。

  • q:slot が指定されていない場合、または空の文字列の場合、コンテンツはデフォルトの <Slot>、つまり name プロパティのない <Slot> に投影されます。
  • 複数の q:slot="footer" 属性は、コンテンツ投影内のアイテムを結合します。

非投影コンテンツ

Qwik は、投影されていない場合でも、すべてのコンテンツを保持します。これは、コンテンツが将来的に投影される可能性があるためです。投影されたコンテンツが <Slot> コンポーネントと一致しない場合、コンテンツは非活性の <q:template> 要素に移動されます。

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
const Accordion = component$(() => {
  const isOpen = useSignal(false);
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  return (
    <Accordion>
      I am pre-rendered on the Server and hidden until needed.
    </Accordion>
  );
});

結果は次のようになります。

<div>
  <h1>▶︎</h1>
</div>
<q:template q:slot hidden aria-hidden="true">
  I am pre-rendered on the Server and hidden until needed.
</q:template>

非投影コンテンツは、非活性の <q:template> に移動されることに注意してください。これは、Accordion コンポーネントが <Slot> で再レンダリングする場合に備えて行われます。この場合、投影されたコンテンツを生成するためだけに親コンポーネントを再レンダリングする必要はありません。親が最初にレンダリングされたときに非投影コンテンツを保持することにより、2 つのコンポーネントのレンダリングを独立した状態に保つことができます。

無効な投影

q:slot 属性は、コンポーネントの直接の子である必要があります。

import { component$ } from '@builder.io/qwik';
 
export const Project = component$(() => { ... })
 
export const MyApp = component$(() => {
  return (
    <Project>
      <span q:slot="title">ok, direct child of Project</span>
      <div>
        <span q:slot="title">Error, not a direct child of Project</span>
      </div>
    </Project>
  );
});

高度な例

編集可能なコンテンツを条件付きで投影する、折りたたみ可能なコンポーネントの例。

import { Slot, component$, useSignal } from '@builder.io/qwik';
 
export const Collapsible = component$(() => {
  const isOpen = useSignal(true);
 
  return (
    <div>
      <h1 onClick$={() => (isOpen.value = !isOpen.value)}>
        {isOpen.value ? '▼' : '▶︎'}
        <Slot name="title" />
      </h1>
      {isOpen.value && <Slot />}
    </div>
  );
});
 
export default component$(() => {
  const title = useSignal('Qwik');
  const description = useSignal(
    'A resumable framework for building instant web applications'
  );
  return (
    <>
      <label>Title</label>
      <input bind:value={title} type="text" />
      <label>Description</label>
      <textarea bind:value={description} cols={50} />
      <hr />
      <Collapsible>
        <span q:slot="title">{title}</span>
        {description}
      </Collapsible>
    </>
  );
});

Collapsible コンポーネントは常にタイトルを表示しますが、テキストの本文は store.isOpentrue の場合にのみ表示されます。

さらに、投影されたコンテンツの titledescription は編集可能です。

  • 親コンポーネントは、Collapsible コンポーネントを強制的に再レンダリングすることなく、コンテンツを変更できる必要があります。
  • 子コンポーネントは、親コンポーネントを再レンダリングすることなく、投影されるものを変更する必要があります。この場合、Collapsible は、親コンポーネントをダウンロードして再レンダリングすることなく、デフォルトの q:slot を表示/非表示にできる必要があります。

2 つのコンポーネントが独立したライフサイクルを持つためには、投影が宣言的である必要があります。このようにして、親または子のいずれかが、他方の再レンダリングを強制することなく、投影されるものや投影方法を変更できます。

投影 vs children

すべてのフレームワークは、コンポーネントが複雑なコンテンツを条件付きでラップする方法を必要とします。この問題はさまざまな方法で解決されますが、主なアプローチは2つあります。

  • プロジェクション: プロジェクションは、コンテンツが親テンプレートから投影される必要のある場所にどのように移動するかを宣言的に記述する方法です。
  • children: childrenは、コンテンツを単なる別の入力として扱うvDOMアプローチを指します。

この2つのアプローチは、宣言型と命令型として最もよく説明できます。どちらにも、それぞれ利点と欠点があります。

Qwikは、親コンポーネントと子コンポーネントを互いに独立してレンダリングできる必要性があるため、宣言的なプロジェクションアプローチを使用します。命令型のアプローチでは、子コンポーネントがchildrenを変更する方法は無数にあります。子コンポーネントがchildrenに依存している場合、変更を再適用するために親コンポーネントが再レンダリングされるたびに再レンダリングを強制されます。この追加のレンダリングは、コンポーネントを分離してレンダリングするというQwikの目標に反します。

注意: 正常に動作させるには、component$()関数内で<Slot />を使用してください。<Slot />は、通常の関数export const MyInlineComp = () =>と同様のインラインコンポーネントでは動作できません。

高度なトピック: スロットとコンテキスト

スロット付きコンポーネントは、投影されていなくても、親コンポーネントのコンテキストにアクセスできます。さらに、親が別のコンポーネント内で<Slot />を投影している場合、スロット付きコンポーネントは、そのより深いコンポーネントのコンテキストにもアクセスできます。

ただし、<Slot />が条件付きでレンダリングされるため、コンポーネントがまだ投影されていない場合、より深いコンテキストについて知ることは不可能であり、スロット付きコンポーネントは直近の親のコンテキストのみを参照します。

そのため、これらの状況を避けるのが最も安全です。コンテキストを提供している場合は、<Slot />を条件付きでレンダリングしないでください。

貢献者

このドキュメントの改善にご協力いただいたすべての貢献者に感謝します!

  • RATIU5
  • manucorporat
  • forresst
  • adamdbradley
  • cunzaizhuyi
  • zanettin
  • lbensaad
  • gabrielgrant
  • mhevery
  • jakovljevic-mladen
  • mrhoodz
  • Jemsco