コンポーネント
コンポーネントは、Qwikアプリケーションの基本的な構成要素です。UIを構築するために使用できる、再利用可能なコードの部分です。
Qwikコンポーネントは、以下のような点がユニークです。
- Qwikコンポーネントは、オプティマイザによって、自動的に遅延読み込みチャンクに分割されます。
- それらは再開可能です(コンポーネントはサーバーで作成され、クライアントで実行を継続できます)。
- それらはリアクティブであり、ページ上の他のコンポーネントとは独立してレンダリングされます。レンダリングを参照してください。
component$()
Qwikコンポーネントは、component$
呼び出しでラップされたJSXを返す関数です。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <div>Hello World!</div>;
});
末尾に
$
が付いたcomponent$
関数は、オプティマイザがコンポーネントを個別のチャンクに分割できるようにします。これにより、各チャンクは必要に応じて個別に読み込まれるようになり、親コンポーネントが読み込まれるたびにすべてのコンポーネントを読み込む必要がなくなります。
注:routesフォルダ内のindex.tsx、layout.tsx、root.tsx、およびすべてのエントリファイルには、**export default**が必要です。その他のコンポーネントには、export constとexport functionを使用できます。
コンポーネントの合成
コンポーネントを組み合わせて、より複雑なコンポーネントを作成できます。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return (
<>
<p>Parent Text</p>
<Child />
</>
);
});
const Child = component$(() => {
return <p>Child Text</p>;
});
Qwikコンポーネントは
$
記号のおかげで既に遅延読み込みされていることに注意してください。つまり、子コンポーネントを手動で動的にインポートする必要はなく、Qwikが自動的に行います。
カウンターの例
カウンターの少し複雑な例です。
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>Increment</button>
</>
);
});
プロップス
プロップスは、親からコンポーネントにデータを渡すために使用されます。プロップスを介して渡されたデータは、component$
関数のprops
引数からアクセスできます。
プロップスは浅い不変性を持つため、プリミティブデータ型(文字列、数値、ブール値)は一度渡されると変更できません。ただし、参照型(オブジェクト、配列、関数)の内部要素は、参照自体が不変であっても変更できます。
子コンポーネントから親コンポーネントでプリミティブプロップスデータを変更するには、シグナルを使用します。子コンポーネント内でローカルにデータを更新する場合は、シグナルは必要ありません。プロップスを分解し、値を使用して新しいローカル変数を定義します。
以下の2つの例は、オプションのname
、quantity
、description
、price
プロップスを宣言するコンポーネントItem
を示しています。
最初の例では、プリミティブデータ型がプロップスを介して渡されます。price
プロップスはシグナルとして渡され、親コンポーネントから変更できます。quantity
は数値として渡され、ローカルでリアクティブに更新できる新しいシグナルをItem
内で定義するために使用されます。あるいは、quantityをリアクティブにする必要がない場合は、シグナルではなく通常の変数として定義することもできます。
import { component$, useSignal } from "@builder.io/qwik";
import type { Signal } from "@builder.io/qwik";
interface ItemProps {
name?: string;
quantity?: number;
description?: string;
price?: Signal<number>;
}
export const Item = component$<ItemProps>((props) => {
const localQuantity = useSignal(props.quantity);
return (
<ul>
<li>name: {props.name}</li>
<li>quantity: {localQuantity}</li>
<li>description: {props.description}</li>
<li>price: {props.price}</li>
</ul>
);
});
export default component$(() => {
const price = useSignal(9.99);
return (
<>
<h1>Props</h1>
<Item name="hammer" price={price} quantity={5} />
</>
);
});
2番目の例では、個々のプリミティブ値ではなく、データを含む単一のdetailsオブジェクトとしてプロップスが渡されます。これにより、シグナルを使用せずに内部のデータを変異させることができます。ただし、データを格納するdetailsオブジェクト自体は依然として不変であり、一度渡されると変更できません。
import { component$ } from "@builder.io/qwik";
interface ItemProps {
details: {
name?: string;
quantity?: number;
description?: string;
price?: number;
};
}
export const Item = component$((props: ItemProps) => {
props.details.price = 4.99;
return (
<ul>
<li>name: {props.details.name}</li>
<li>quantity: {props.details.quantity}</li>
<li>description: {props.details.description}</li>
<li>price: {props.details.price}</li>
</ul>
);
});
export default component$(() => {
return (
<Item
details={{ name: "hammer", quantity: 5, description: "", price: 9.99 }}
/>
);
});
上記の例では、プロップスの明示的な型を提供するために
component<ItemProps>
を使用しています。これはオプションですが、TypeScriptコンパイラがプロップスが正しく使用されていることを確認できるようにします。
デフォルトのプロップス
プロップスのデストラクチャリングパターンを使用して、デフォルト値を提供できます。
interface Props {
enabled?: boolean;
placeholder?: string;
}
// We can use JS's destructuring of props to provide a default value.
export default component$<Props>(({enabled = true, placeholder = ''}) => {
return (
<input disabled={!enabled} placeholder={placeholder} />
);
});
リアクティブ性に基づくレンダリング
Qwikコンポーネントはリアクティブです。つまり、状態の変化に応じて自動的に更新されます。2種類の更新があります。
- 状態はDOMテキストまたは属性にバインドされています。このような変更は通常、DOMを直接更新し、コンポーネント関数の再実行は必要ありません。
- 状態はDOMに構造的な変更を引き起こします(要素が作成または削除されます)。このような変更には、コンポーネント関数の再実行が必要です。
覚えておくべきことは、状態が変更されると、状態がバインドされているものに応じて、コンポーネント関数が0回以上実行される可能性があることです。このため、関数はべき等である必要があり、実行回数に依存するべきではありません。
状態の変化により、コンポーネントが無効になります。コンポーネントが無効になると、無効化キューに追加され、次のrequestAnimationFrame
でフラッシュ(レンダリング)されます。これは、コンポーネントレンダリングの合体の一種として機能します。
DOM要素の取得
DOM要素を取得するにはref
を使用します。DOM要素を格納するシグナルを作成します。次に、シグナルをJSXのref
プロパティに渡します。
DOM要素への参照を取得することは、要素サイズ(
getBoundingClientRect
)、計算されたスタイルの取得、WebGLキャンバスの初期化、あるいはDOM要素と直接やり取りするサードパーティライブラリの接続などに役立ちます。
import { component$, useVisibleTask$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const width = useSignal(0);
const height = useSignal(0);
const outputRef = useSignal<Element>();
useVisibleTask$(() => {
if (outputRef.value) {
const rect = outputRef.value.getBoundingClientRect();
width.value = Math.round(rect.width);
height.value = Math.round(rect.height);
}
});
return (
<section>
<article
ref={outputRef}
style={{ border: '1px solid red', width: '100px' }}
>
Change text value here to stretch the box.
</article>
<p>
The above red box is {height.value} pixels high and {width.value}{' '}
pixels wide.
</p>
</section>
);
});
id
による要素へのアクセス
サーバーとクライアント環境間でのサーバーとクライアントの環境では、要素にid
でアクセスする必要がある場合があります。useId()
関数を用いると、サーバーサイドレンダリング(SSR)とクライアントサイドの操作で一貫性を保つ、現在のコンポーネントの一意の識別子を取得できます。これは、サーバーレンダリングされたコンポーネントがクライアント側のスクリプトを必要とする場合(例:以下)に重要です。
- アニメーションエンジン
- アクセシビリティの向上
- その他のクライアントサイドライブラリ
複数のフラグメントが同時に実行されるマイクロフロントエンドのセットアップでは、useId()
は実行環境全体で一意かつ一貫したIDを保証し、競合を排除します。
import {
component$,
useId,
useSignal,
useVisibleTask$,
} from '@builder.io/qwik';
export default component$(() => {
const elemIdSignal = useSignal<string | null>(null);
const id = useId();
const elemId = `${id}-example`;
console.log('server-side id:', elemId);
useVisibleTask$(() => {
const elem = document.getElementById(elemId);
elemIdSignal.value = elem?.getAttribute('id') || null;
console.log('client-side id:', elemIdSignal.value);
});
return (
<section>
<div id={elemId}>
Both server-side and client-side console should match this id:
<br />
<b>{elemIdSignal.value || null}</b>
</div>
</section>
);
});
遅延読み込み
このコンポーネントは、バンドル目的で親子関係を分断する場合にも重要な役割を果たします。
export const Child = () => <span>child</span>;
const Parent = () => (
<section>
<Child />
</section>
);
上記の例では、Parent
コンポーネントを参照すると、Child
コンポーネントへの推移的な参照が暗示されます。バンドラがチャンクを作成する際、Parent
への参照はChild
もバンドルする必要があります。(Parent
は内部的にChild
を参照しています。)これらの推移的な依存関係は問題です。なぜなら、ルートコンポーネントへの参照があると、アプリケーションの残りの部分も推移的に参照されることを意味するからです。Qwikはこれを明示的に回避しようとします。
上記の問題を回避するために、コンポーネントを直接参照するのではなく、遅延ラッパーを参照します。これはcomponent$()
関数によって自動的に作成されます。
import { component$ } from '@builder.io/qwik';
export const Child = component$(() => {
return <p>child</p>;
});
export const Parent = component$(() => {
return (
<section>
<Child />
</section>
);
});
export default Parent;
上記の例では、オプティマイザによって以下のように変換されます。
const Child = componentQrl(qrl('./chunk-a', 'Child_onMount'));
const Parent = componentQrl(qrl('./chunk-b', 'Parent_onMount'));
const Parent_onMount = () => qrl('./chunk-c', 'Parent_onRender');
const Parent_onRender = () => (
<section>
<Child />
</section>
);
注記 簡潔にするため、すべての変換は表示されていません。結果の記号はすべて同じファイルに保持されます。
オプティマイザがコードを変換した後、Parent
はChild
を直接参照しなくなっていることに注意してください。これは重要です。なぜなら、バンドラ(およびツリーシェイカー)が、アプリケーションの残りの部分を引っ張ることなく、記号を自由に異なるチャンクに移動できるようになるからです。
では、Parent
コンポーネントがChild
コンポーネントをレンダリングする必要があるが、Child
コンポーネントがまだダウンロードされていない場合はどうなるでしょうか?まず、Parent
コンポーネントは次のようにDOMをレンダリングします。
<main>
<section>
<!--qv--><!--/qv-->
</section>
</main>
上記の例でわかるように、<!--qv-->
は、Child
コンポーネントが遅延読み込みされた後に挿入されるマーカーとして機能します。
インラインコンポーネント
すべての遅延読み込みプロパティを持つ標準的なcomponent$()
に加えて、Qwikは、従来のフレームワークのコンポーネントのように動作する軽量な(インライン)コンポーネントもサポートしています。
import { component$ } from '@builder.io/qwik';
// Inline component: declared using a standard function.
export const MyButton = (props: { text: string }) => {
return <button>{props.text}</button>;
};
// Component: declared using `component$()`.
export default component$(() => {
return (
<p>
Some text:
<MyButton text="Click me" />
</p>
);
});
上記の例では、MyButton
はインラインコンポーネントです。標準的なcomponent$()
とは異なり、インラインコンポーネントは個別に遅延読み込みできません。代わりに、親コンポーネントとバンドルされます。この場合、
MyButton
はdefault
コンポーネントとバンドルされます。default
がレンダリングされるたびに、MyButton
もレンダリングされます。
インラインコンポーネントは、インスタンス化されたコンポーネントにインライン化されていると考えてください。
制限事項
インラインコンポーネントには、標準的なcomponent$()
にはないいくつかの制限があります。インラインコンポーネントは
useSignal
やuseStore
などのuse*
メソッドを使用できません。<Slot>
でコンテンツを投影できません。
名前が示すように、インラインコンポーネントは、親コンポーネントとバンドルされるという利便性を提供するため、軽量なマークアップの一部に控えめに使用するのが最適です。
ポリモーフィックコンポーネント
propsに応じて異なるタイプの要素を出力したいが、デフォルトでは<div>
を使用したい場合は、次のような方法を使用できます。
const Poly = component$(
<C extends string | FunctionComponent = 'div'>({
as,
...props
}: { as?: C } & PropsOf<string extends C ? 'div' : C>) => {
const Cmp = as || 'div';
return (
<Cmp {...props}>
<Slot />
</Cmp>
);
}
);
export const TestComponent = component$(() => {
// These all work with correct types
return (
<>
<Poly>Hello from a div</Poly>
<Poly as="a" href="/blog">
Blog
</Poly>
<Poly as="input" onInput$={(ev, el) => console.log(el.value)} />
<Poly as={OtherComponent} someProp />
</>
);
});
string extends C
に注目してください。これは、TypeScriptがas
プロップから型を推論できない場合にのみ当てはまり、デフォルトの型を指定できます。
APIの概要
状態
useSignal(initialState)
- リアクティブな値を作成しますuseStore(initialStateObject)
- 状態を格納するために使用できるリアクティブなオブジェクトを作成しますcreateContextId(contextName)
- コンテキスト参照を作成しますuseContextProvider()
- 特定のコンテキストに値を提供しますuseContext()
- 現在のコンテキストの値を読み取ります
スタイル
useStylesScoped$()
- スコープ付きスタイルをコンポーネントに追加しますuseStyles$()
- スコープなしスタイルをコンポーネントに追加します
イベント
useOn()
- プログラムで現在のコンポーネントにリスナーを追加しますuseOnWindow()
- プログラムでウィンドウオブジェクトにリスナーを追加しますuseOnDocument()
- プログラムでドキュメントオブジェクトにリスナーを追加します
タスク/ライフサイクル
useTask$()
- レンダリング前および/または監視対象のストアが変更されたときに呼び出されるコールバックを定義しますuseVisibleTask$()
- クライアント側(ブラウザ)でのみ、レンダリング後に呼び出されるコールバックを定義しますuseResource$()
- 非同期でデータを読み込むリソースを作成します
その他
$()
- QRLを作成しますnoSerialize()
useErrorBoundary()
コンポーネント
<Slot>
- コンテンツ投影スロットを宣言します<SSRStreamBlock>
- ストリームブロックを宣言します<SSRStream>
- ストリームを宣言します<Fragment>
- JSXフラグメントを宣言します