QWIK DOCS / V2

表单与变更

用 routeAction$ 与 <Form> 处理写操作,没有 JavaScript 也能提交。

约 3 分钟Qwik v213 / 19
本页目录4 节
  1. <Form> 的双重身份
  2. 校验与失败
  3. 代码中调用
  4. routeAction$ 与 globalAction$

routeAction$() 定义只在服务端执行、且只在被显式调用时运行的操作。它适合写数据库、发邮件、创建订单这类有副作用的逻辑。

tsxsrc/routes/signup/index.tsx
import { component$ } from '@qwik.dev/core';
import { routeAction$, Form } from '@qwik.dev/router';

export const useSignup = routeAction$(async (data) => {
  const user = await db.users.create({
    name: data.name,
    email: data.email,
  });
  return { success: true, id: user.id };
});

export default component$(() => {
  const signup = useSignup();
  return (
    <Form action={signup}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">创建账号</button>
      {signup.value?.success && <p>欢迎,{signup.value.id}</p>}
    </Form>
  );
});

01<Form> 的双重身份#

  • 没有 JavaScript 时,它就是一个普通 HTML 表单,浏览器直接 POST 到 action 端点。
  • 有 JavaScript 时,它拦截提交,在 SPA 中调用 action,不刷新页面。
  • action 完成后,可自动刷新被 invalidate 的 loader,让页面数据保持一致。

提交过程中 action.isRunning 为 true,适合禁用按钮或显示进度。action.formData 保留上一次提交的字段,刷新后也能回填。

tsxcode
<button type="submit" disabled={signup.isRunning}>
  {signup.isRunning ? '提交中…' : '创建账号'}
</button>

02校验与失败#

Qwik Router 内置与 Zod 的集成:zod$() 在 action 执行前校验表单,类型会自动收窄;失败信息进入 fieldErrors。zod$ 从 @qwik.dev/router 导入,回调参数就是 zod 实例(项目里需要先装 zod)。

tsxcode
import { routeAction$, zod$ } from '@qwik.dev/router';

export const useSignup = routeAction$(
  async (data) => {
    const user = await db.users.create(data);
    return { success: true, id: user.id };
  },
  zod$((z) => ({
    name: z.string().min(2),
    email: z.string().email(),
  }))
);
tsxcode
<input name="email" />
{signup.value?.failed && (
  <p role="alert">{signup.value.fieldErrors.email}</p>
)}

业务层失败使用 fail(status, data) 返回,而不是抛异常。这样页面仍能渲染,用户看到的是可恢复的错误提示。

tsxcode
if (!user) {
  return fail(409, { message: '邮箱已被注册' });
}

03代码中调用#

不是所有提交都来自表单。action.submit(data) 返回 Promise,适合按钮触发的操作或先做客户端校验。

tsxcode
const action = useDeleteTodo();

<button
  onClick$={async () => {
    const { value } = await action.submit({ id: todo.id });
    if (value.failed) return;
    todos.value = todos.value.filter((t) => t.id !== todo.id);
  }}
>
  删除
</button>

04routeAction$ 与 globalAction$#

routeAction$globalAction$
声明位置routes 下的 index/layoutsrc 任意位置
作用域当前路由全站
典型场景受保护页面、路由相关写操作登录、全局搜索、共享操作

表单提交后想跳转,在 action 中 throw redirect(303, '/done')。当 JavaScript 关闭时,这会形成 Post/Redirect/Get 流程,刷新结果页不会重复提交。