如果 routeLoader$ 是“读数据”,那 routeAction$ 就是“写数据”:
- 提交表单
- 点赞
- 发评论
- 修改资料
- 下单
- 发邮件
- 写数据库
官方文档把它说得很清楚:
action 只在显式调用时执行,而且只在服务端执行。
这很关键,因为“写操作”“副作用”本来就不该在渲染阶段乱跑。
来看一个完整例子:
import { component$ } from '@builder.io/qwik';
import {
Form,
routeAction$,
routeLoader$,
z,
zod$,
} from '@builder.io/qwik-city';
export const useArticle = routeLoader$(async ({ params }) => {
return db.article.findBySlug(params.slug);
});
export const useCreateComment = routeAction$(
async (data, { params }) => {
await db.comment.create({
articleSlug: params.slug,
content: data.content,
});
return {
success: true,
};
},
zod$({
content: z.string().min(2, '评论至少 2 个字'),
})
);
export default component$(() => {
const article = useArticle();
const commentAction = useCreateComment();
return (
<article>
<h1>{article.value.title}</h1>
<Form action={commentAction}>
<textarea name="content" placeholder="写下你的评论" />
{commentAction.value?.failed && (
<p>{commentAction.value.fieldErrors?.content}</p>
)}
<button type="submit">发表评论</button>
</Form>
{commentAction.value?.success && <p>评论已提交</p>}
</article>
);
});#这里最妙的地方是什么?
<Form /> 的底层是原生 HTML <form>。
这意味着:
- 没 JS 也能提交
- 有 JS 时,Qwik City 会拦截提交,给你 SPA 体验
- 但它不是“必须靠客户端框架接管一切”才成立
这非常符合 Qwik 的城区哲学:
优先保留原生 Web 能力,再在其上做可恢复增强。
#routeAction$ 和 routeLoader$ 的关系
页面进来
└── routeLoader$ 读数据
用户提交表单
└── routeAction$ 写数据
写完后页面更新
└── 相关 loaders 可重新执行所以你可以粗暴地记:
loader = 读action = 写
先别混。