Open4

Qwikでの開発メモ

Hi MORISHIGEHi MORISHIGE

Qwikで小さなアプリをいくつか作ったので主にReactやReactを利用したフレームワークとの違いと気づきをまとめていく。
https://qwik.builder.io/
大前提としてQwikはReactではないが、JSXを利用している分、Reactに慣れている人には入りやすい。
他のJSフレームワークとの大きな違いとしてハイドレーションがないのが特徴であったり、Resumabilityなど目新しい思想や仕組みについてはたくさんの情報があるのでそちらにおまかせ。

Hi MORISHIGEHi MORISHIGE

State

https://qwik.builder.io/docs/components/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++; }),
});
Hi MORISHIGEHi MORISHIGE

環境変数

https://qwik.builder.io/docs/env-variables/

環境変数の扱いはBuild-time変数とServer-side変数の2種類。

Build-time変数

ビルド時にバンドルされるためサーバーサイド、ブラウザサイドで利用可能。接頭辞としてPUBLIC_が必要。

.env
PUBLIC_API_URL=https://api.example.com

利用例

src/routes/index.tsx
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変数

サーバーサイドでのランタイムでのみ利用可能な環境変数。セキュアな値はこちらで。

.env.local
DB_PRIVATE_KEY=123456789

利用例

src/routes/index.tsx
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'));
});
Hi MORISHIGEHi MORISHIGE

サーバーサイドでのデータ取得

Qwik Cityでの開発でデータを取得する方法は多岐にわたる。
一番利用頻度が高いのはrouteLoader$()

  • Endpoints
  • routeLoader$()

ちなみにデータの取得順序としては、Endpoints -> routeLoader$()となる。

Endpoints

onRequestonGetonPostonPutonDeleteonPatchonHead

主にAPIエンドポイントとして利用する場合に利用する。通常のページindex.tsxlayout.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することも可能だが現在はうまく動作していない)

src/routes/product/[productId]/index.tsx
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>;
}