Qwikでの開発メモ
Qwikで小さなアプリをいくつか作ったので主にReactやReactを利用したフレームワークとの違いと気づきをまとめていく。
他のJSフレームワークとの大きな違いとしてハイドレーションがないのが特徴であったり、Resumabilityなど目新しい思想や仕組みについてはたくさんの情報があるのでそちらにおまかせ。
State
Qwikではリアクティブなstateを扱うためにuseSignal()
とuseStore()
の2種類。
ReactだとuseState()
と同じような扱いだが初期値によって2種類にわけられている。
ドキュメント上ではプリミティブな値とオブジェクトで使い分けるようになっているが、useSignal()
にオブジェクトや配列を渡すことも可能。
複数のStateを扱う場合にuseStore()
を利用するというイメージが妥当かもしれない。
useSignal()
useSignal()
はプリミティブな値を初期値にもつ。
値はSIgnalオブジェクトのvalue
に格納される。
import { component$, useSignal } from '@builder.io/qwik';
export default component$(() => {
const count = useSignal(0);
return (
<button onClick$={() => count.value++}>
Increment {count.value}
</button>
);
});
useStore()
useStore()
はオブジェクトを初期値にもつ。ドキュメント上ではオブジェクトを初期値とあるが、配列も利用できる。
useSinal()
とは異なりオブジェクト自体を操作する。
import { component$, useStore } from '@builder.io/qwik';
export default component$(() => {
const state = useStore({ count: 0 });
return (
<>
<button onClick$={() => state.count++}>Increment</button>
<p>Count: {state.count}</p>
</>
);
});
useStore()
はすべてのネストされた値をプロキシするためパフォーマンス観点からdeep: false
を利用してトップレベルのみ監視対象とすることも検討しておく。
const shallowStore = useStore(
{
nested: {
fields: { are: 'also tracked' }
},
list: [],
},
{ deep: false}
);
扱うことができるのはシリアライズできるものだけ。関数を渡す場合は$()
を利用したQwikのQRL形式に置き換えて渡すことが可能。
const state = useStore({
count: 0,
increment: $(function () { this.count++; }),
});
環境変数
環境変数の扱いはBuild-time変数とServer-side変数の2種類。
Build-time変数
ビルド時にバンドルされるためサーバーサイド、ブラウザサイドで利用可能。接頭辞としてPUBLIC_
が必要。
PUBLIC_API_URL=https://api.example.com
利用例
import { component$ } from '@builder.io/qwik';
export default component$(() => {
// `({}).PUBLIC_*` variables can be read anywhere, including browser
return <div>PUBLIC_API_URL: {import.meta.env.PUBLIC_API_URL}</div>
})
Server-side変数
サーバーサイドでのランタイムでのみ利用可能な環境変数。セキュアな値はこちらで。
DB_PRIVATE_KEY=123456789
利用例
import {
routeLoader$,
routeAction$,
server$,
type RequestEvent,
} from '@builder.io/qwik-city';
export const onGet = (requestEvent: RequestEvent) => {
console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
};
export const onPost = (requestEvent: RequestEvent) => {
console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
};
export const useAction = routeAction$(async (_, requestEvent) => {
console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
});
export const useLoader = routeLoader$(async (requestEvent) => {
console.log(requestEvent.env.get('DB_PRIVATE_KEY'));
});
export const serverFunction = server$(function () {
// `this` is the `RequestEvent` object
console.log(this.env.get('DB_PRIVATE_KEY'));
});
サーバーサイドでのデータ取得
Qwik Cityでの開発でデータを取得する方法は多岐にわたる。
一番利用頻度が高いのはrouteLoader$()
。
- Endpoints
routeLoader$()
ちなみにデータの取得順序としては、Endpoints -> routeLoader$()
となる。
Endpoints
onRequest
、onGet
、onPost
、onPut
、onDelete
、onPatch
、onHead
主にAPIエンドポイントとして利用する場合に利用する。通常のページindex.tsx
やlayout.tsx
でも利用可能。その際はheaderの制御やcookieのsetなどmiddleware的に利用することもできる。
JSONを返すエンドポイントとして
import type { RequestHandler } from '@builder.io/qwik-city';
// Called when the HTTP method is GET
export const onGet: RequestHandler = async (requestEvent) => {
// Respond with a JSON object
requestEvent.json(200, { hello: 'world' });
};
Cookieの制御
import type { RequestHandler } from '@builder.io/qwik-city';
export const onGet: RequestHandler = async (requestEvent) => {
// Read a cookie
requestEvent.cookie.get('my-cookie');
// Set a cookie
requestEvent.cookie.set('my-cookie', 'Hello World');
};
routeLoader$()
Qwikコンポーネントレンダリング中に利用できるデータを取得、処理する関数。処理完了までレンダリングをブロックすることになる。(defer()
やuseResource$()
などでstreamすることも可能だが現在はうまく動作していない)
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
export const useProductDetails = routeLoader$(async (requestEvent) => {
// This code runs only on the server, after every navigation
const res = await fetch(`https://.../products/${requestEvent.params.productId}`);
const product = await res.json();
return product as Product;
});
export default component$(() => {
// In order to access the `routeLoader$` data within a Qwik Component, you need to call the hook.
const signal = useProductDetails(); // Readonly<Signal<Product>>
return <p>Product name: {signal.value.product.name}</p>;
});
routeLoader$()
との連携
他のrequestEvent.resolveValue()
を利用することで他のrouteLoader$()
の解決を待ってから処理を続けることが可能になる。その分レンダリングをブロックする時間は伸びるので注意。
export const useProductRecommendations = routeLoader$(async (requestEvent) => {
// Resolve the product details from the other loader
const product = await requestEvent.resolveValue(useProductDetails);
// Use the product details to fetch personalized data
const res = fetch(`https://.../recommendations?product=${product.id}`);
const recommendedProducts = (await res.json()) as Product[];
return recommendedProducts;
});
RequestEventオブジェクト
Endpoint、routeLoader$()
で利用できるRequestEvent
オブジェクトを活用しない手はない。
RequestEventオブジェクト
export interface RequestEvent {
/**
* HTTP response headers.
*
* https://developer.mozilla.org/en-US/docs/Glossary/Response_header
*/
readonly headers: Headers;
/**
* HTTP request and response cookie. Use the `get()` method to retrieve a request cookie value.
* Use the `set()` method to set a response cookie value.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
*/
readonly cookie: Cookie;
/**
* HTTP request method.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
*/
readonly method: string;
/**
* URL pathname. Does not include the protocol, domain, query string (search params) or hash.
*
* https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname
*/
readonly pathname: string;
/**
* URL path params which have been parsed from the current url pathname segments.
* Use `query` to instead retrieve the query string search params.
*/
readonly params: Readonly<Record<string, string>>;
/**
* URL Query Strings (URL Search Params).
* Use `params` to instead retrieve the route params found in the url pathname.
*
* https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
*/
readonly query: URLSearchParams;
/**
* HTTP request URL.
*/
readonly url: URL;
/**
* The base pathname of the request, which can be configured at build time.
* Defaults to `/`.
*/
readonly basePathname: string;
/**
* HTTP request information.
*/
readonly request: Request;
/**
* Platform specific data and functions
*/
readonly platform: PLATFORM;
/**
* Platform provided environment variables.
*/
readonly env: EnvGetter;
/**
* Shared Map across all the request handlers. Every HTTP request will get a new instance of
* the shared map. The shared map is useful for sharing data between request handlers.
*/
readonly sharedMap: Map<string, any>;
/**
* This method will check the request headers for a `Content-Type` header and parse the body accordingly.
* It supports `application/json`, `application/x-www-form-urlencoded`, and `multipart/form-data` content types.
*
* If the `Content-Type` header is not set, it will return `null`.
*/
readonly parseBody: () => Promise<unknown>;
/**
* Convenience method to set the Cache-Control header.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
*/
readonly cacheControl: (cacheControl: CacheControl) => void;
/**
* HTTP response status code. Sets the status code when called with an
* argument. Always returns the status code, so calling `status()` without
* an argument will can be used to return the current status code.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
*/
readonly status: (statusCode?: number) => number;
/**
* Which locale the content is in.
*
* The locale value can be retrieved from selected methods using `getLocale()`:
*/
readonly locale: (local?: string) => string;
/**
* URL to redirect to. When called, the response will immediately
* end with the correct redirect status and headers.
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
*/
readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage;
/**
* When called, the response will immediately end with the given
* status code. This could be useful to end a response with `404`,
* and use the 404 handler in the routes directory.
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* for which status code should be used.
*/
readonly error: (statusCode: number, message: string) => ErrorResponse;
/**
* Convenience method to send an text body response. The response will be automatically
* set the `Content-Type` header to`text/plain; charset=utf-8`.
* An `text()` response can only be called once.
*/
readonly text: (statusCode: number, text: string) => AbortMessage;
/**
* Convenience method to send an HTML body response. The response will be automatically
* set the `Content-Type` header to`text/html; charset=utf-8`.
* An `html()` response can only be called once.
*/
readonly html: (statusCode: number, html: string) => AbortMessage;
/**
* Convenience method to JSON stringify the data and send it in the response.
* The response will be automatically set the `Content-Type` header to
* `application/json; charset=utf-8`. A `json()` response can only be called once.
*/
readonly json: (statusCode: number, data: any) => AbortMessage;
/**
* Send a body response. The `Content-Type` response header is not automatically set
* when using `send()` and must be set manually. A `send()` response can only be called once.
*/
readonly send: SendMethod;
/**
* Low-level access to write to the HTTP response stream. Once `getWritableStream()` is called,
* the status and headers can no longer be modified and will be sent over the network.
*/
readonly getWritableStream: () => WritableStream<Uint8Array>;
readonly next: () => Promise<void>;
}