Qwik入門
コンポーネント
QwikコンポーネントはReactコンポーネントと非常によく似ています。JSXを返す関数です。ただし、component$(...)
を使用する必要があり、イベントハンドラには$
サフィックスを付ける必要があり、状態はuseSignal()
を使用して作成され、class
はclassName
の代わりに使用され、その他にもいくつかの違いがあります。
import { component$, Slot } from '@builder.io/qwik';
import type { ClassList } from '@builder.io/qwik'
export const MyOtherComponent = component$((props: { class?: ClassList }) => { // ✅
return <div class={class}><Slot /></div>;
});
import { component$, useSignal } from '@builder.io/qwik';
// Other components can be imported and used in JSX.
import { MyOtherComponent } from './my-other-component';
interface MyComponentProps {
step: number;
}
// Components are always declared with the `component$` function.
export const MyComponent = component$((props: MyComponentProps) => {
// Components use the `useSignal` hook to create reactive state.
const count = useSignal(0); // { value: 0 }
return (
<>
<button
onClick$={() => {
// Event handlers have the `$` suffix.
count.value = count.value + props.step;
}}
>
Increment by {props.step}
</button>
<main
class={{
even: count.value % 2 === 0,
odd: count.value % 2 === 1,
}}
>
<h1>Count: {count.value}</h1>
<MyOtherComponent class="correct-way"> {/* ✅ */}
{count.value > 10 && <p>Count is greater than 10</p>}
{count.value > 10 ? <p>Count is greater than 10</p> : <p>Count is less than 10</p>}
</MyOtherComponent>
</main>
</>
);
});
アイテムのリストのレンダリング
Reactと同様に、map
関数を使用してアイテムのリストをレンダリングできますが、リスト内の各アイテムには一意のkey
プロパティが必要です。key
は文字列または数値でなければならず、リスト内では一意である必要があります。
import { component$, useSignal } from '@builder.io/qwik';
import { US_PRESIDENTS } from './presidents';
export const PresidentsList = component$(() => {
return (
<ul>
{US_PRESIDENTS.map((president) => (
<li key={president.number}>
<h2>{president.name}</h2>
<p>{president.description}</p>
</li>
))}
</ul>
);
});
イベントハンドラの再利用
イベントハンドラは、JSXノード間で再利用できます。これは、$(...handler...)
を使用してハンドラを作成することで行われます。
import { $, component$, useSignal } from '@builder.io/qwik';
interface MyComponentProps {
step: number;
}
// Components are always declared with the `component$` function.
export const MyComponent = component$(() => {
const count = useSignal(0);
// Notice the `$(...)` around the event handler function.
const inputHandler = $((event, elem) => {
console.log(event.type, elem.value);
});
return (
<>
<input name="name" onInput$={inputHandler} />
<input
name="password"
onInput$={inputHandler}
/>
</>
);
});
コンテンツプロジェクション
コンテンツプロジェクションは、@builder.io/qwik
からエクスポートされる<Slot/>
コンポーネントによって行われます。スロットには名前を付けることができ、q:slot
属性を使用して投影できます。
// File: src/components/Button/Button.tsx
import { component$, Slot } from '@builder.io/qwik';
import styles from './Button.module.css';
export const Button = component$(() => {
return (
<button class={styles.button}>
<div class={styles.start}>
<Slot name="start" />
</div>
<Slot />
<div class={styles.end}>
<Slot name="end" />
</div>
</button>
);
});
export default component$(() => {
return (
<Button>
<span q:slot="start">📩</span>
Hello world
<span q:slot="end">🟩</span>
</Button>
);
});
useフックのルール
use
で始まるメソッドは、useSignal()
、useStore()
、useOn()
、useTask$()
、useLocation()
など、Qwikでは特別なものです。Reactフックと非常によく似ています。
- コンポーネント$内でのみ呼び出すことができます。
- 条件式やループ内ではなく、コンポーネント$の最上位レベルからのみ呼び出すことができます。
スタイル
Qwikは、CSSモジュール、Tailwind、グローバルCSSインポート、useStylesScoped$()
を使用した遅延読み込みスコープ付きCSSをすぐにサポートしています。CSSモジュールは、Qwikコンポーネントをスタイル設定するための推奨方法です。
CSSモジュール
CSSモジュールを使用するには、.module.css
ファイルを作成します。たとえば、src/components/MyComponent/MyComponent.module.css
です。
.container {
background-color: red;
}
次に、コンポーネントにCSSモジュールをインポートします。
import { component$ } from '@builder.io/qwik';
import styles from './MyComponent.module.css';
export default component$(() => {
return <div class={styles.container}>Hello world</div>;
});
Qwikは、CSSクラスにclassName
ではなくclass
を使用することに注意してください。
$(...)ルール
$(...)
関数と$
で終わる関数は、$()
、useTask$()
、useVisibleTask$()
など、Qwikでは特殊です。末尾の$
は遅延読み込み境界を表します。任意の$
関数の最初の引数には、いくつかのルールが適用されます。これはjQueryとは全く関係ありません。
- 最初の引数はインポートされた変数でなければなりません。
- 最初の引数は、同じモジュールの最上位レベルで宣言された変数でなければなりません。
- 最初の引数は、任意の変数の式でなければなりません。
- 最初の引数が関数の場合、同じモジュールの最上位レベルで宣言されている変数、または値がシリアライズ可能な変数のみをキャプチャできます。シリアライズ可能な値には、
string
、number
、boolean
、null
、undefined
、Array
、Object
、Date
、RegExp
、Map
、Set
、BigInt
、Promise
、Error
、JSXノード
、Signal
、Store
、さらにはHTMLElementsが含まれます。
// Valid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedFunction } from './my-other-module';
export function exportedFunction() {
console.log('exported function');
}
export default component$(() => {
// The first argument is a function.
const valid1 = $((event, elem) => {
console.log(event.type, elem.value);
});
// The first argument is an imported identifier.
const valid2 = $(importedFunction);
// The first argument is an identifier declared at the top level of the same module.
const valid3 = $(exportedFunction);
// The first argument is an expression without local variables.
const valid4 = $([1, 2, { a: 'hello' }]);
// The first argument is a function that captures a local variable.
const localVariable = 1;
const valid5 = $((event) => {
console.log('local variable', localVariable);
});
});
無効な$
関数の例をいくつか示します。
// Invalid examples of `$` functions.
import { $, component$, useSignal } from '@builder.io/qwik';
import { importedVariable } from './my-other-module';
export default component$(() => {
const unserializable = new CustomClass();
const localVariable = 1;
// The first argument is a local variable.
const invalid1 = $(localVariable);
// The first argument is a function that captures an unserializable local variable.
const invalid2 = $((event) => {
console.log('custom class', unserializable);
});
// The first argument is an expression that uses a local variable.
const invalid3 = $(localVariable + 1);
// The first argument is an expression that uses an imported variable.
const invalid4 = $(importedVariable + 'hello');
});
リアクティブ状態
useSignal(initialValue?)
useSignal()
は、リアクティブな状態を作成する主な方法です。シグナルはコンポーネント間で共有でき、シグナルを読み取るコンポーネントまたはタスク(signal.value
を実行する)は、シグナルが変更されるとレンダリングされます。
// Typescript definition for `Signal<T>` and `useSignal<T>`
export interface Signal<T> {
value: T;
}
export const useSignal: <T>(value?: T | (() => T)): Signal<T>;
useSignal(initialValue?)
は、オプションの初期値を受け取り、Signal<T>
オブジェクトを返します。Signal<T>
オブジェクトには、読み書きできるvalue
プロパティがあります。コンポーネントまたはタスクがvalue
プロパティにアクセスすると、自動的にサブスクリプションが作成されるため、value
が変更されると、value
を読み取るすべてのコンポーネント、タスク、または他の計算されたシグナルが再評価されます。
useStore(initialValue?)
useStore(initialValue?)
はuseSignal
に似ていますが、リアクティブなJavaScriptオブジェクトを作成するという点が異なります。これにより、オブジェクトのプロパティごとに、シグナルのvalue
と同様に、リアクティブ性が実現されます。内部的には、useStore
はすべてのプロパティアクセスをインターセプトするProxy
オブジェクトを使用して実装されており、プロパティをリアクティブにします。
// Typescript definition `useStore<T>`
// The `Reactive<T>` is a reactive version of the `T` type, every property of `T` behaves like a `Signal<T>`.
export interface Reactive<T extends Record<string, any>> extends T {}
export interface StoreOptions {
// If `deep` is true, then nested property of the store will be wrapped in a `Signal<T>`.
deep?: boolean;
}
export const useStore: <T>(value?: T | (() => T), options?: StoreOptions): Reactive<T>;
実際には、useSignal
とuseStore
は非常に似ており(useSignal(0) === useStore({ value: 0 })
)、ほとんどの場合、useSignal
の方が好ましいです。useStore
の使用例をいくつか挙げます。
- 配列でリアクティビティが必要な場合。
- プロパティを簡単に追加できるリアクティブなオブジェクトが必要な場合。
import { component$, useStore } from '@builder.io/qwik';
export const Counter = component$(() => {
// The `useStore` hook is used to create a reactive store.
const todoList = useStore(
{
array: [],
},
{ deep: true }
);
// todoList.array is a reactive array, so we can push to it and the component will re-render.
return (
<>
<h1>Todo List</h1>
<ul>
{todoList.array.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onInput$={() => {
// todoList is a reactive store
// because we used `deep: true`, the `todo` object is also reactive.
// so we can change the `completed` property and the component will re-render.
todo.completed = !todo.completed;
}}
/>
{todo.text}
</li>
))}
</ul>
</>
);
});
useTask$(() => { ... })
useTask$
は非同期タスクを作成するために使用されます。タスクは副作用の実装、重い計算の実行、レンダリングライフサイクルの一部としての非同期コードの実行に役立ちます。useTask$
タスクは最初のレンダリングの前に実行され、その後、追跡されたシグナルまたはストアが変更されるたびに、タスクは再実行されます。
import { component$, useSignal, useTask$ } from '@builder.io/qwik';
export const Counter = component$(() => {
const page = useSignal(0);
const listOfUsers = useSignal([]);
// The `useTask$` hook is used to create a task.
useTask$(() => {
// The task is executed before the first render.
console.log('Task executed before first render');
});
// You can create multiple tasks, and they can be async.
useTask$(async (taskContext) => {
// Since we want to re-run the task whenever the `page` changes,
// we need to track it.
taskContext.track(() => page.value);
console.log('Task executed before the first render AND when page changes');
console.log('Current page:', page.value);
// Tasks can run async code, such as fetching data.
const res = await fetch(`https://api.randomuser.me/?page=${page.value}`);
const json = await res.json();
// Assigning to a signal will trigger a re-render.
listOfUsers.value = json.results;
});
return (
<>
<h1>Page {page.value}</h1>
<ul>
{listOfUsers.value.map((user) => (
<li key={user.login.uuid}>
{user.name.first} {user.name.last}
</li>
))}
</ul>
<button onClick$={() => page.value++}>Next Page</button>
</>
);
});
useTask$()
はSSR中はサーバーで、コンポーネントがクライアントに最初にマウントされた場合はブラウザで実行されます。そのため、サーバーでは使用できないため、タスク内でDOM APIにアクセスするのは良い考えではありません。代わりに、イベントハンドラまたはuseVisibleTask$()
を使用して、クライアント/ブラウザでのみタスクを実行する必要があります。
useVisibleTask$(() => { ... })
useVisibleTask$
は、コンポーネントがDOMに最初にマウントされた直後に発生するタスクを作成するために使用されます。useTask$
に似ていますが、クライアントでのみ実行され、最初のレンダリング後に実行されます。レンダリング後に実行されるため、DOMの検査やブラウザAPIの使用が可能です。
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
export const Clock = component$(() => {
const time = useSignal(0);
// The `useVisibleTask$` hook is used to create a task that runs eagerly on the client.
useVisibleTask$((taskContext) => {
// Since this VisibleTask is not tracking any signals, it will only run once.
const interval = setInterval(() => {
time.value = new Date();
}, 1000);
// The `cleanup` function is called when the component is unmounted, or when the task is re-run.
taskContext.cleanup(() => clearInterval(interval));
});
return (
<>
<h1>Clock</h1>
<h1>Seconds passed: {time.value}</h1>
</>
);
});
Qwikはユーザー操作前にブラウザでJavaScriptを実行しないため、useVisibleTask$()
はクライアントで熱心に実行される唯一のAPIであり、そのため、次のような操作を行うのに適した場所です。
- DOM APIへのアクセス
- ブラウザ専用のライブラリの初期化
- 分析コードの実行
- アニメーションまたはタイマーの開始。
useVisibleTask$()
はサーバーでは実行されないため、データの取得には使用しないでください。代わりに、useTask$()
を使用してデータを取得し、useVisibleTask$()
を使用してアニメーションの開始などの操作を行う必要があります。useVisibleTask$()
の乱用はパフォーマンスの低下につながる可能性があります。
ルーティング
Qwikにはファイルベースのルーターが付属しており、Next.jsに似ていますが、いくつかの違いがあります。ルーターはファイルシステム、特にsrc/routes/
に基づいています。src/routes/
の下のフォルダに新しいindex.tsx
ファイルを作成すると、新しいルートが作成されます。たとえば、src/routes/home/index.tsx
は/home/
にルートを作成します。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <h1>Home</h1>;
});
コンポーネントをデフォルトエクスポートとしてエクスポートすることが非常に重要です。そうでないと、ルーターはコンポーネントを見つけることができません。
ルートパラメータ
ルートパスに[param]
を含むフォルダを追加することで、動的ルートを作成できます。たとえば、src/routes/user/[id]/index.tsx
は/user/:id/
にルートを作成します。ルートパラメータにアクセスするには、@builder.io/qwik-city
からエクスポートされたuseLocation
フックを使用できます。
import { component$ } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
export default component$(() => {
const loc = useLocation();
return (
<main>
{loc.isNavigating && <p>Loading...</p>}
<h1>User: {loc.params.userID}</h1>
<p>Current URL: {loc.url.href}</p>
</main>
);
});
useLocation()
はリアクティブなRouteLocation
オブジェクトを返し、ルートが変更されるたびに再レンダリングされます。RouteLocation
オブジェクトには、次のプロパティがあります。
/**
* The current route location returned by `useLocation()`.
*/
export interface RouteLocation {
readonly params: Readonly<Record<string, string>>;
readonly url: URL;
readonly isNavigating: boolean;
}
他のルートへのリンク
他のルートへのリンクを作成するには、@builder.io/qwik-city
からエクスポートされたLink
コンポーネントを使用できます。Link
コンポーネントは、<a>
HTMLAnchorElementのプロパティをすべて受け取ります。唯一の違いは、完全なページナビゲーションを行う代わりに、Qwikルーターを使用してSPAナビゲーションでルートに移動することです。
import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
export default component$(() => {
return (
<>
<h1>Home</h1>
<Link href="/about/">SPA navigate to /about/</Link>
<a href="/about/">Full page navigate to /about/</a>
</>
);
});
データの取得/読み込み
サーバーからデータを読み込む推奨方法は、@builder.io/qwik-city
からエクスポートされたrouteLoader$()
関数を使用することです。routeLoader$()
関数は、ルートがレンダリングされる前にサーバーで実行されるデータローダーを作成するために使用されます。routeLoader$()
の戻り値は、ルートファイルから名前付きエクスポートとしてエクスポートする必要があります。つまり、src/routes/
内のindex.tsx
でのみ使用できます。
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
// The `routeLoader$()` function is used to create a data loader that will be executed on the server before the route is rendered.
// The return of `routeLoader$()` is a custom use hook, which can be used to access the data returned from `routeLoader$()`.
export const useUserData = routeLoader$(async (requestContext) => {
const user = await db.table('users').get(requestContext.params.userID);
return {
name: user.name,
email: user.email,
};
});
export default component$(() => {
// The `useUserData` hook will return a `Signal` containing the data returned from `routeLoader$()`, which will re-render the component, whenever the navigation changes, and the routeLoader$() is re-run.
const userData = useUserData();
return (
<main>
<h1>User data</h1>
<p>User name: {userData.value.name}</p>
<p>User email: {userData.value.email}</p>
</main>
);
});
// Exported `head` function is used to set the document head for the route.
export const head: DocumentHead = ({resolveValue}) => {
// It can use the `resolveValue()` method to resolve the value from `routeLoader$()`.
const user = resolveValue(useUserData);
return {
title: `User: "${user.name}"`,
meta: [
{
name: 'description',
content: 'User page',
},
],
};
};
routeLoader$()
関数は、Promiseを返す関数を取得します。Promiseはサーバーで解決され、解決された値はuseCustomLoader$()
フックに渡されます。useCustomLoader$()
フックは、routeLoader$()
関数によって作成されるカスタムフックです。useCustomLoader$()
フックは、routeLoader$()
関数から返されたPromiseの解決済み値を含むSignal
を返します。useCustomLoader$()
フックは、ルートが変更され、routeLoader$()
関数が再実行されるたびにコンポーネントを再レンダリングします。
フォーム送信の処理
Qwikは、サーバーでフォームリクエストを処理するために、@builder.io/qwik-city
からエクスポートされたrouteAction$()
APIを提供します。routeAction$()
は、フォームが送信されたときにサーバーでのみ実行されます。
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
// The `routeAction$()` function is used to create a data loader that will be executed on the server when the form is submitted.
// The return of `routeAction$()` is a custom use hook, which can be used to access the data returned from `routeAction$()`.
export const useUserUpdate = routeAction$(async (data, requestContext) => {
const user = await db.table('users').get(requestContext.params.userID);
user.name = data.name;
user.email = data.email;
await db.table('users').put(user);
return {
user,
};
}, zod$({
name: z.string(),
email: z.string(),
}));
export default component$(() => {
// The `useUserUpdate` hook will return an `ActionStore<T>` containing the `value` returned from `routeAction$()`, and some other properties, such as `submit()`, which is used to submit the form programmatically, and `isRunning`. All of these properties are reactive, and will re-render the component whenever they change.
const userData = useUserUpdate();
// userData.value is the value returned from `routeAction$()`, which is `undefined` before the form is submitted.
// userData.formData is the form data that was submitted, it is `undefined` before the form is submitted.
// userData.isRunning is a boolean that is true when the form is being submitted.
// userData.submit() is a function that can be used to submit the form programmatically.
// userData.actionPath is the path to the action, which is used to submit the form.
return (
<main>
<h1>User data</h1>
<Form action={userData}>
<div>
<label>User name: <input name="name" defaultValue={userData.formData?.get('name')} /></label>
</div>
<div>
<label>User email: <input name="email" defaultValue={userData.formData?.get('email')} /></label>
</div>
<button type="submit">Update</button>
</Form>
</main>
);
});
routeAction$()
は、@builder.io/qwik-city
からエクスポートされたForm
コンポーネントと組み合わせて使用されます。Form
コンポーネントは、ネイティブHTML<form>
要素をラップしたものです。Form
コンポーネントは、action
プロパティとしてActionStore<T>
を取得します。ActionStore<T>
は、routeAction$()
関数の戻り値です。
ブラウザでのみコードを実行する
Qwikはサーバーとブラウザで同じコードを実行するため、サーバーでコードが実行されると存在しないため、コードでwindow
やその他のブラウザAPIを使用することはできません。
window
、document
、localStorage
、sessionStorage
、webgl
などのブラウザAPIにアクセスする場合は、ブラウザAPIにアクセスする前にコードがブラウザで実行されているかどうかを確認する必要があります。
import { component$, useTask$, useVisibleTask$, useSignal } from '@builder.io/qwik';
import { isBrowser } from '@builder.io/qwik/build';
export default component$(() => {
const ref = useSignal<Element>();
// useVisibleTask$ will only run in the browser
useVisibleTask$(() => {
// No need to check for `isBrowser` before accessing the DOM, because useVisibleTask$ will only run in the browser
ref.value?.focus();
document.title = 'Hello world';
});
// useTask might run on the server, so you need to check for `isBrowser` before accessing the DOM
useTask$(() => {
if (isBrowser) {
// This code will only run in the browser only when the component is first rendered there
ref.value?.focus();
document.title = 'Hello world';
}
});
return (
<button
ref={ref}
onClick$={() => {
// All event handlers are only executed in the browser, so it's safe to access the DOM
ref.value?.focus();
document.title = 'Hello world';
}}
>
Click me
</button>
);
});
useVisibleTask$(() => { ... })
このAPIは、クライアント/ブラウザでのみ実行されることが保証されているVisibleTaskを宣言します。サーバーでは決して実行されません。
JSXイベントハンドラ
onClick$
やonInput$
などのJSXハンドラは、クライアントでのみ実行されます。これらはDOMイベントであるためです。サーバーにDOMがないため、サーバーでは実行されません。
サーバーでのみコードを実行する
データの取得やデータベースへのアクセスなど、サーバーでのみコードを実行する必要がある場合があります。この問題を解決するために、Qwikはサーバーでのみコードを実行するためのいくつかのAPIを提供します。
import { component$, useTask$ } from '@builder.io/qwik';
import { server$, routeLoader$ } from '@builder.io/qwik/qwik-city';
import { isServer } from '@builder.io/qwik/build';
export const useGetProducts = routeLoader$((requestEvent) => {
// This code will only run on the server
const db = await openDB(requestEvent.env.get('DB_PRIVATE_KEY'));
const product = await db.table('products').select();
return product;
})
const encryptOnServer = server$(function(message: string) {
// `this` is the `requestEvent
const secretKey = this.env.get('SECRET_KEY');
const encryptedMessage = encrypt(message, secretKey);
return encryptedMessage;
});
export default component$(() => {
useTask$(() => {
if () {
// This code will only run on the server only when the component is first rendered in the server
}
});
return (
<>
<button
onClick$={server$(() => {
// This code will only run on the server when the button is clicked
})}
>
Click me
</button>
<button
onClick$={() => {
// This code will call the server function, and wait for the result
const encrypted = await encryptOnServer('Hello world');
console.log(encrypted);
}}
>
Click me
</button>
</>
);
});
routeAction$()
routeAction$()
は、サーバーでのみ実行される特別なコンポーネントです。フォームの送信やその他の操作を処理するために使用されます。たとえば、これを用いてユーザーをデータベースに追加し、ユーザーのプロファイルページにリダイレクトできます。
routeLoader$()
routeLoader$()
は、サーバーでのみ実行される特別なコンポーネントです。データを取得し、ページをレンダリングするために使用されます。たとえば、APIからデータを取得し、そのデータを使用してページをレンダリングするために使用できます。
server$((...args) => { ... })
server$()
は、サーバーでのみ実行される関数を宣言する特別な方法です。クライアントから呼び出された場合、RPC呼び出しのように動作し、サーバーで実行されます。シリアライズ可能な引数を任意に受け取り、シリアライズ可能な値を任意に返すことができます。
isServer
& isBrowser
条件文
if(typeof window !== 'undefined')
の代わりに、@builder.io/qwik/build
からエクスポートされるisServer
とisBrowser
のブール型のヘルパーを使用することをお勧めします。これにより、コードがブラウザでのみ実行されることが保証されます。ブラウザ環境をより適切に検出するための、より堅牢なチェックが含まれています。
参照のためのソースコードです。
export const isBrowser: boolean = /*#__PURE__*/ (() =>
typeof window !== 'undefined' &&
typeof HTMLElement !== 'undefined' &&
!!window.document &&
String(HTMLElement).includes('[native code]'))();
export const isServer: boolean = !isBrowser;
参照のために、これらをインポートする方法です。
import {isServer, isBrowser} from '@builder.io/qwik/build';
// inside component$
useTask$(({ track }) => {
track(() => interactionSig.value) <-- tracks on the client when a signal has changed.
// server code
if (isServer) return;
// client code here
});
//