routeAction$()

routeAction$() は、サーバー上で排他的に、かつ明示的に呼び出された場合にのみ実行されるアクションと呼ばれる関数を定義するために使用されます。アクションは、データベースへの書き込みやメールの送信など、クライアントサイドレンダリング中には発生できない副作用を持つことができます。そのため、フォームの送信、副作用のある操作の実行、そしてクライアント/ブラウザにデータを戻してUIの更新に使用できるという点で理想的です。

アクションは、@builder.io/qwik-cityからエクスポートされるrouteAction$()またはglobalAction$()を使用して宣言できます。

src/routes/layout.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (data, requestEvent) => {
  // This will only run on the server when the user submits the form (or when the action is called programmatically)
  const userID = await db.users.add({
    firstName: data.firstName,
    lastName: data.lastName,
  });
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
 
  return (
    <>
      <Form action={action}>
        <input name="firstName" />
        <input name="lastName" />
        <button type="submit">Add user</button>
      </Form>
      {action.value?.success && (
        // When the action is done successfully, the `action.value` property will contain the return value of the action
        <p>User {action.value.userID} added successfully</p>
      )}
    </>
  );
});

アクションはレンダリング中に実行されないため、データベースへの書き込みやメールの送信などの副作用を持つことができます。アクションは明示的に呼び出された場合にのみ実行されます。

<Form/> でのアクションの使用

アクションを呼び出す最善の方法は、@builder.io/qwik-cityでエクスポートされる<Form/>コンポーネントを使用することです。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button type="submit">Add user</button>
      {action.value?.success && <p>User added successfully</p>}
    </Form>
  );
});

内部的には、<Form/>コンポーネントはネイティブのHTML<form>要素を使用しているため、JavaScriptなしでも動作します。

JSが有効になっている場合、<Form/>コンポーネントはフォームの送信をインターセプトし、SPAモードでアクションをトリガーします。完全なSPAエクスペリエンスを実現できます。

これは、サーバーがページ全体を再レンダリングし、すべてを再実行することを明確にするためです。そのため、routeLoader$ がある場合、それらも実行されます。

複雑なフォームはドット表記を使用して作成できます。

プログラムによるアクションの使用

アクションは、action.submit()メソッドを使用してプログラムでトリガーすることもできます(つまり、<Form/>コンポーネントは必要ありません)。ただし、関数と同様に、ボタンクリックやその他のイベントからアクションをトリガーできます。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$ } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <section>
      <button
        onClick$={async () => {
          const { value } = await action.submit({ name: 'John' });
          console.log(value);
        }}
      >
        Add user
      </button>
      {action.value?.success && <p>User added successfully</p>}
    </section>
  );
});

上記の例では、ユーザーがボタンをクリックするとaddUserアクションがトリガーされます。action.submit()メソッドは、アクションが完了すると解決されるPromiseを返します。

イベントハンドラー付きのアクション

onSubmitCompleted$イベントハンドラーは、アクションが正常に実行され、データが返された後に使用できます。これは、アクションが完了したら、UI要素のリセットやアプリケーション状態の更新などのタスクを実行する場合に役立ちます。

これは、todoアプリのEditFormコンポーネントでアイテムを編集するために使用されるonSubmitCompleted$ハンドラーの例です。

src/components/EditForm.tsx
import { component$, type Signal, useSignal } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
import { type ListItem, useEditFromListAction } from '../../routes/index';
 
export interface EditFormProps {
  item: listItem;
  editingIdSignal: Signal<string>;
}
 
const EditForm = component$(
  ({ item, editingIdSignal }: EditFormProps) => {
    const editAction = useEditFromListAction();
 
    return (
      <div>
        <Form
          action={editAction}
          onSubmitCompleted$={() => {
            editingIdSignal.value = '';
          }}
          spaReset
        >
          <input
            type="text"
            value={item.text}
            name="text"
            id={`edit-${item.id}`}
          />
          {/* Sends item.id with form data on submission. */}
          <input type="hidden" name="id" value={item.id} />
          <button type="submit">
            Submit
          </button>
        </Form>
 
        <div>
          <button onClick$={() => (editingIdSignal.value = '')}>
            Cancel
          </button>
        </div>
      </div>
    );
  }
);
 
export default EditForm;

この例では、onSubmitCompleted$を使用して、フォームの送信が正常に完了したらeditingIdSignal値を空文字列にリセットします。これにより、アプリケーションは状態を更新し、デフォルトビューに戻ることができます。

バリデーションと型安全

Qwikは、zod$()関数を使用して直接アクションで使用できるTypeScriptファーストスキーマバリデーションであるZodを組み込みでサポートしています。

アクション + Zod を使用すると、アクションが実行される前にサーバー側でデータが検証される型安全なフォームを作成できます。

src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user) => {
    // The "user" is strongly typed: { firstName: string, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // Zod schema is used to validate that the FormData includes "firstName" and "lastName"
  zod$({
    firstName: z.string(),
    lastName: z.string(),
  })
);
 
export default component$(() => {
  const action = useAddUser();
  return (
    <>
      <Form action={action}>
        <input name="firstName" />
        <input name="lastName" />
 
        {action.value?.failed && <p>{action.value.fieldErrors?.firstName}</p>}
        <button type="submit">Add user</button>
      </Form>
      {action.value?.success && (
        <p>User {action.value.userID} added successfully</p>
      )}
    </>
  );
});

routeAction()にデータを送信すると、データはZodスキーマに対して検証されます。データが無効な場合、アクションは検証エラーをrouteAction.valueプロパティに配置します。

Zod のドキュメント を参照して、Zod スキーマの使用方法の詳細を確認してください。

高度なイベントベースのバリデーション

zod$ のコンストラクタは、最初の引数として zod 自体を取得する関数も受け取ることができるため、これを使用して直接スキーマを構築できます。2 番目のパラメータは、イベントベースの zod スキーマを構築するための RequestEvent です。特に zod の refinesuperDefine と組み合わせることで、可能性は無限大です。

高度なイベントベースのバリデーション
export const useAddUser = routeAction$(
  async (user) => {
    // The "user" is still strongly typed, but firstname 
    // is now optional: { firstName?: string | undefined, lastName: string }
    const userID = await db.users.add({
      firstName: user.firstName,
      lastName: user.lastName,
    });
    return {
      success: true,
      userID,
    };
  },
  // Zod schema is used to validate that the FormData includes "firstName" and "lastName"
  zod$((z, ev) => {
    // The first name is optional if the url contains the query parameter "firstname=optional"
    const firstName =
      ev.url.searchParams.get("firstname") === "optional"
        ? z.string().optional()
        : z.string().nonempty();
 
    return z.object({
      firstName,
      lastName: z.string(),
    });
  })
);

HTTP リクエストとレスポンス

routeAction$globalAction$ は、現在の HTTP リクエストとレスポンスに関する情報を含む RequestEvent オブジェクトにアクセスできます。

これにより、アクションは routeAction$ 関数内でリクエストヘッダー、クッキー、URL、および環境変数にアクセスできます。

src/routes/product/[user]/index.tsx
import { routeAction$ } from '@builder.io/qwik-city';
 
// The second argument of the action is the `RequestEvent` object
export const useProductRecommendations = routeAction$(
  async (_data, requestEvent) => {
    console.log('Request headers:', requestEvent.request.headers);
    console.log('Request cookies:', requestEvent.cookie);
    console.log('Request url:', requestEvent.url);
    console.log('Request params:', requestEvent.params);
    console.log('MY_ENV_VAR:', requestEvent.env.get('MY_ENV_VAR'));
  }
);
 

アクションの失敗

成功以外の値を返すには、アクションで fail() メソッドを使用する必要があります。

import { routeAction$, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user, { fail }) => {
    // `user` is typed { name: string }
    const userID = await db.users.add(user);
    if (!userID) {
      return fail(500, {
        message: 'User could not be added',
      });
    }
    return {
      userID,
    };
  },
  zod$({
    name: z.string(),
  })
);

失敗は、成功値と同様に action.value プロパティに格納されます。ただし、アクションが失敗すると、action.value.failed プロパティは true に設定されます。さらに、失敗メッセージは、Zod スキーマで定義されたプロパティに従って fieldErrors オブジェクトに見つけることができます。

fieldErrors はドット表記のオブジェクトになります。詳細については、複雑なフォーム を参照してください。

import { component$ } from '@builder.io/qwik';
import { Form } from '@builder.io/qwik-city';
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" />
      <button type="submit">Add user</button>
      {action.value?.failed && <p>{action.value.fieldErrors.name}</p>}
      {action.value?.userID && <p>User added successfully</p>}
    </Form>
  );
});

TypeScript の型判別のおかげで、action.value.failed プロパティを使用して成功と失敗を判別できます。

以前のフォームの状態

アクションがトリガーされると、以前の状態が action.formData プロパティに格納されます。これは、アクションの実行中にローディング状態を表示するのに役立ちます。

import { component$ } from '@builder.io/qwik';
import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  // handle action...
});
 
export default component$(() => {
  const action = useAddUser();
  return (
    <Form action={action}>
      <input name="name" value={action.formData?.get('name')} />
      <button type="submit">Add user</button>
    </Form>
  );
});

action.formData は、ページのリフレッシュ後でもユーザーが入力したフォームデータを保持するために特に役立ちます。これにより、JSが無効になっている場合でも、シームレスなSPAエクスペリエンスを実現できます。

ルートアクションとグローバルアクション

アクションは、@builder.io/qwik-city からエクスポートされる routeAction$() または globalAction$() を使用して宣言できます。両者の違いは、routeAction$() はルートにスコープされるのに対し、globalAction$() はアプリ全体でグローバルに使用できることです。

routeAction$() から始めることをお勧めします。複数のルートでアクションを共有する場合、またはルートではないコンポーネントでアクションを使用する場合は、globalAction$() のみを使用してください。

routeAction$()

routeAction$() は、src/routes フォルダー内の layout.tsx または index.tsx ファイルでのみ宣言でき、routeLoader$() と同様にエクスポートする必要があります。routeAction$() は宣言されたルート内でのみアクセスできるため、アクションがユーザーデータにアクセスする必要がある場合、または保護されたルートである場合に推奨されます。「プライベート」アクションと考えてください。

共通の再利用可能なrouteAction$()を管理したい場合は、この関数を既存のルートの'layout.tsx'または'index.tsx'ファイルから再エクスポートすることが不可欠です。そうでなければ実行されなかったり、例外が発生します。詳細については、クックブックを参照してください。

src/routes/form/index.tsx
import { routeAction$ } from '@builder.io/qwik-city';
 
export const useChangePassword = routeAction$((data) => {
  // ...
});

globalAction$()

globalAction$() は、src フォルダー内のどこでも宣言できます。globalAction$() はグローバルに使用できるため、複数のルートでアクションを共有する必要がある場合、またはアクションがユーザーデータにアクセスする必要がない場合に推奨されます。たとえば、ユーザーをログインさせる useLogin アクションなどです。「パブリック」アクションと考えてください。

src/components/login/login.tsx
import { globalAction$ } from '@builder.io/qwik-city';
 
export const useLogin = globalAction$((data) => {
  // ...
});

貢献者

このドキュメントの改善に貢献してくださった皆様に感謝いたします!

  • manucorporat
  • cunzaizhuyi
  • forresst
  • keuller
  • hamatoyogi
  • AnthonyPAlicea
  • the-r3aper7
  • thejackshelton
  • adnanebrahimi
  • mhevery
  • ulic75
  • CoralWombat
  • tzdesign
  • igorbabko
  • gioboa
  • mrhoodz
  • VinuB-Dev
  • aivarsliepa
  • wtlin1228
  • adamdbradley
  • gioboa
  • jemsco
  • tzdesign