Getting Started Qwik
はじめに
Qwikのドキュメント Getting Started Qwiklyにある チュートリアルのQwik Joke Appを進めながらジョークアプリを作っていきます。
前提条件
QwikをDocker環境で構築する で構築した環境を使ってやります。
デモアプリがすでに入っている場合は、src/routes/layout.tsx
を修正してスタイルを初期状態します
import { component$, Slot, useStyles$ } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import type { RequestHandler } from "@builder.io/qwik-city";
- import Header from "../components/starter/header/header";
- import Footer from "../components/starter/footer/footer";
- import styles from "./styles.css?inline";
export const onGet: RequestHandler = async ({ cacheControl }) => {
// Control caching for this request for best performance and to reduce hosting costs:
// https://qwik.dev/docs/caching/
cacheControl({
// Always serve a cached response by default, up to a week stale
staleWhileRevalidate: 60 * 60 * 24 * 7,
// Max once every 5 seconds, revalidate on the server to get a fresh version of this page
maxAge: 5,
});
};
export const useServerTimeLoader = routeLoader$(() => {
return {
date: new Date().toISOString(),
};
});
export default component$(() => {
- useStyles$(styles);
return (
<>
- <Header />
<main>
<Slot />
</main>
- <Footer />
</>
);
});
Qwik Joke App
Qwikのチュートリアルでは、Qwik でジョーク アプリを構築する手順を進めしながら、Qwik の最も重要な概念について理解していきます。このアプリは、https://icanhazdadjoke.comからランダムにジョークを取得し、ボタンをクリックすると新しいジョークが表示されるようにします。
1.ルートの作成
まず、初めに特定のルートでページを作成します。この基本的なアプリは、ルート/joke/でランダムなお父さんジョークのアプリケーションをつくります。このチュートリアルはディレクトリベースのルーティングを使うQwikのメタフレームワークであるQwikcityに依存しています。
1.プロジェクトのroutes
ディレクトリに、joke
ディレクトリとindex.tsx
ファイルを作成します。
2.各ルートのindex.tsxファイルには、Qwikcity がどのコンテンツを出力するかわかるように export default component$(...)
が必要です 。
import { component$ } from '@builder.io/qwik';
export default component$(() => {
return <section class="section bright">A Joke!</section>
});
3.ブラウザでlocalhost:5173/joke/
にアクセスして、新しいページが表示していることを確認します。
メモ
- jokeの デフォルトコンポーネントは、既存のレイアウトに囲まれています。レイアウトについての詳細は、Layoutを参照してください。
-
index.tsx
、routes
フォルダのlayout.tsx
、root.tsx
、すべてのエントリーファイルにはexport default
が必要です。その他のコンポーネントには、export const
とexport function
を使用できます。 - コンポーネントの作成方法の詳細については、Component API を参照してください。
2.データ読込
https://icanhazdadjoke.comのJSON API
を使ってランダムなジョークを読み込みます。ルート ローダーを使用してデータをサーバーにロードし、コンポーネントでレンダリングします。
1.src/routes/joke/index.tsx
に次のコードを追加します
import { component$ } from '@builder.io/qwik';
+ import { routeLoader$ } from '@builder.io/qwik-city';
+ export const useDadJoke = routeLoader$(async () => {
+ const response = await fetch('https://icanhazdadjoke.com/', {
+ headers: { Accept: 'application/json' },
+ });
+ return (await response.json()) as {
+ id: string;
+ status: number;
+ joke: string;
+ };
+ });
export default component$(() => {
- return <section class="section bright">A Joke!</section>
+ // `useDadJoke`フックを呼び出すと、ロードされたデータに対してリアクティブ・シグナルが返される。
+ const dadJokeSignal = useDadJoke();
+ return (
+ <section class="section bright">
+ <p>{dadJokeSignal.value.joke}</p>
+ </section>
+ );
});
2.http://localhost:5173/joke/
にアクセスする度にランダムなジョークが表示されます。
メモ
-
routeLoader$
に渡された関数は、コンポーネントがレンダリングされる前にサーバー上で呼び出され、データロードをします。 -
routeLoader$
は、useDadJoke()
フックを返します。 -
routeLoader$
は、どのコンポーネントでもuse-hook
が起動されなくても、コンポーネントがレンダリングされる前にサーバー上で起動されます。 -
routeLoader$
の戻り値の型は、追加の型情報を必要とせずに、コンポーネント内で推測されます。
3.サーバーへデータ送信
これまでは、サーバからクライアントにデータを送信するために、routeLoader$
というコンポーネントが使われていました。クライアントからサーバにデータをポスト(送信)するには、routeAction$
を使います。
注:routeAction$
は、ブラウザのネイティブフォームAPIを使用するため、JavaScriptが無効になっていても動作するので、サーバにデータを送信する方法として推奨されます。
1.アクションを宣言するには、次のコードを追加します。
import { routeLoader$, Form, routeActions$ } from '@builder.io/qwik-city';
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
2.export default
コンポーネントを更新し、<Form>
でuseJokeVoteAction
フックを使用するようにします。
export default component$(() => {
const dadJokeSigbnal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">👍</button>
<button name="vote" value="down">👎</button>
</Form>
</section>
);
});
3.これで http://localhost:5173/joke/ にボタンが表示され、クリックすると値をコンソールに表示します。
メモ
-
routeAction$
はデータを受け取ります。-
routeAction$
に渡された関数は、フォームが投稿されるたびにサーバー上で呼び出されます。 -
routeAction$
はuseJokeVoteAction
というuse
フックを返し、フォームデータを投稿するコンポーネントで使うことができます。
-
-
Form
は、ブラウザのネイティブ<form>
要素をラップする便利なコンポーネントです。 -
バリデーションについては、zod validationを参照してください。
-
routeAction$
はJavaScriptが無効でも動作します。 -
JavaScriptが有効な場合、
Form
コンポーネントはブラウザがフォームを投稿しないようにし、代わりにJavaScriptを使ってデータを投稿し、ブラウザのネイティブフォームの動作をエミュレートします。
このセクションのコードは次のとおりです。
import { component$ } from '@builder.io/qwik';
- import { routeLoader$ } from '@builder.io/qwik-city';
+ import { routeLoader$, Form, routeActions$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
+ export const useJokeVoteAction = routeAction$((props) => {
+ console.log('VOTE', props);
+ });
export default component$(() => {
// `useDadJoke`フックを呼び出すと、ロードされたデータに対してリアクティブ・シグナルが返される。
const dadJokeSigbnal = useDadJoke();
+ const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
+ <Form action={favoriteJokeAction}>
+ <input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
+ <button name="vote" value="up">
+ 👍
+ </button>
+ <button name="vote" value="down">
+ 👎
+ </button>
+ </Form>
</section>
);
});
4.状態管理
状態を追跡し、UI を更新することは、アプリケーションの中心的な機能です。Qwik は、アプリケーションの状態を追跡するためのuseSignal
フックを提供します。詳細については、「状態管理」を参照してください。
1.qwik
からuseSignal
をインポートします。
import { component$, useSignal } from "@builder.io/qwik";
2.useSignal()
を使用してコンポーネントの状態を宣言します。
const isFavoriteSignal = useSignal(false);
3.Form
タグの後に、状態を変更するためのボタンをコンポーネントに追加します。
<button
onClick$={() => {
isFavoriteSignal.value = !isFavoriteSignal.value;
}}>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
ボタンをクリックすると状態が更新され、UI も更新されます。
このセクションのコードは次のとおりです。
- import { component$ } from '@builder.io/qwik';
+ import { component$, useSignal } from '@builder.io/qwik';
import { routeLoader$, Form, routeActions$ } from '@builder.io/qwik-city';
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
+ const isFavoriteSignal = useSignal(false);
// `useDadJoke`フックを呼び出すと、ロードされたデータに対してリアクティブ・シグナルが返される。
const dadJokeSigbnal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
+ <button
+ onClick$ = {() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
+ >
+ {isFavoriteSignal.value ? '❤️' : '🤍'}
+ </button>
</section>
);
});
5.タスクとサーバーコードの呼び出し
task
は状態が変化したときに実行する処理です。(これは他のフレームワークのeffect
に似ています。)
この例では、task
を使用してサーバー上のコードを呼び出します。
1.qwik
からuseTask$
, qwik-city
からserver$
をインポートします。
import { component$, useSignak, useTask$ } from "@builder.io/qwik";
import {
routeLoader$,
From,
routeAction$,
server$
} from "@builder.io/qwik-city";
2.isFavoriteSignal
の状態を追跡する新しいタスクを作成します。
useTask$(({ track }) => {});
3.isFavoriteSignal
の状態の変化時にタスクを再実行するための呼び出しtrack
を追加します。
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
});
4.状態の変更時に実行する処理を追加します。
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSingnal.value);
});
5.サーバー上のみ処理を実行したい場合は、server$()
をラップします。
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSingnal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
メモ
-
useTask$
の本体は、サーバーとクライアントの両方で実行されます。 - SSRでは、サーバーは
FAVORITE (isomorphic) false
およびFAVORITE (server) false
を出力します。 - ユーザーがお気に入りを操作すると、クライアントは
FAVORITE (isomorphic) true
を表示し、サーバーはFAVORITE (server) true
を表示します。
このセクションのコードは次のとおりです。
- import { component$, useSignal } from '@builder.io/qwik';
+ import { component$, useSignak, useTask$ } from "@builder.io/qwik";
- import { routeLoader$, Form, routeActions$ } from '@builder.io/qwik-city';
+ import {
+ routeLoader$,
+ From,
+ routeAction$,
+ server$
+ } from "@builder.io/qwik-city";
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
const isFavoriteSignal = useSignal(false);
// `useDadJoke`フックを呼び出すと、ロードされたデータに対してリアクティブ・シグナルが返される。
const dadJokeSigbnal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
+ useTask$(({ track }) => {
+ track(() => isFavoriteSignal.value);
+ console.log('FAVORITE (isomorphic)', isFavoriteSingnal.value);
+ server$(() => {
+ console.log('FAVORITE (server)', isFavoriteSignal.value);
+ })();
+ });
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$ = {() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
6.スタイリング
スタイルは、アプリケーションにとって重要です。スタイルをコンポーネントに関連付けてスコープを設定する方法になります。
スタイルを追加するには:
1.新しいファイル src/routes/joke/index.css
を作成します。
p {
font-weight: bold;
}
from {
float: right;
}
2.src/routes/joke/index.tsx
にスタイルを追加します
import styles from "./index.css?inline";
3.qwik
に useStylesScoped$
を追加します
import { component$, useSignal, useStylesScoped$, useTask$ } from "@builder.o/qwik";
4.コンポーネントにスタイルをロードします
useStylesScoped$(styles);
メモ
-
?inline
クエリパラメータは、Vite にスタイルをコンポーネントにインライン化するようにします。 -
useStylesScoped$
の呼び出しは、スタイルをコンポーネントのみ関連付けるようにします (スコープ設定)。 - スタイルは、SSR の一部としてインライン化されていない場合にのみ、最初のコンポーネントに対してのみ読み込まれます。
このセクションのコードは次のとおりです。
- import { component$, useSignak, useTask$ } from "@builder.io/qwik";
+ import {
+ component$,
+ useSignal,
+ useStylesScoped$,
+ useTask$
+ } from "@builder.o/qwik";
import {
routeLoader$,
From,
routeAction$,
server$
} from "@builder.io/qwik-city";
+ import styles from "./index.css?inline";
export const useDadJoke = routeLoader$(async () => {
const response = await fetch('https://icanhazdadjoke.com/', {
headers: { Accept: 'application/json' },
});
return (await response.json()) as {
id: string;
status: number;
joke: string;
};
});
export const useJokeVoteAction = routeAction$((props) => {
console.log('VOTE', props);
});
export default component$(() => {
+ useStylesScoped$(styles);
const isFavoriteSignal = useSignal(false);
// `useDadJoke`フックを呼び出すと、ロードされたデータに対してリアクティブ・シグナルが返される。
const dadJokeSigbnal = useDadJoke();
const favoriteJokeAction = useJokeVoteAction();
useTask$(({ track }) => {
track(() => isFavoriteSignal.value);
console.log('FAVORITE (isomorphic)', isFavoriteSingnal.value);
server$(() => {
console.log('FAVORITE (server)', isFavoriteSignal.value);
})();
});
return (
<section class="section bright">
<p>{dadJokeSignal.value.joke}</p>
<Form action={favoriteJokeAction}>
<input type="hidden" name="jokeID" value={dadJokeSignal.value.id} />
<button name="vote" value="up">
👍
</button>
<button name="vote" value="down">
👎
</button>
</Form>
<button
onClick$ = {() => (isFavoriteSignal.value = !isFavoriteSignal.value)}
>
{isFavoriteSignal.value ? '❤️' : '🤍'}
</button>
</section>
);
});
7.プレビュー
このチュートリアルには、主要な Qwik の概念と API の概要として、基本的なサンプル アプリケーションが含まれています。アプリケーションは開発モードで実行され、ホット モジュール リロード (HMR) を使用して、コードを変更しながらアプリケーションを継続的に更新します。
開発モードの場合:
- 各ファイルは個別に読み込まれるため、ネットワーク タブにウォーターフォールが発生する可能性があります。
- バンドルの推測的な読み込みは行われないため、最初のやり取りで遅延が発生する可能性があります。
これらの問題を排除した本番ビルドを作成しましょう。
プレビュー ビルドを作成するには:
- npm run preview を実行して本番ビルドを作成します。
メモ
- これで、アプリケーションは別のポートで本番ビルドを実行するようになります。
- ここでアプリケーションを操作すると、開発ツールのネットワーク タブに、ServiceWorkerキャッシュからバンドルが即座に配信されていることが表示されるはずです。
Discussion