routeAction$()
Действия позволяют обрабатывать отправленные формы, позволяя выполнять побочные эффекты, такие как запись в базу данных или отправка электронного письма.
Действия также могут возвращать данные обратно клиенту/браузеру, позволяя соответствующим образом обновлять пользовательский интерфейс, т.е. отображение сообщения об успехе после отправки формы.
Действия могут быть объявлены с помощью routeAction$()
или globalAction$()
, экспортируемых из @builder.io/qwik-city
.
import { component$ } from '@builder.io/qwik';
import { routeAction$, Form } from '@builder.io/qwik-city';
export const useAddUser = routeAction$(async (data, requestEvent) => {
// Этот код будет выполняться на сервере только тогда, когда пользователь отправит форму (или когда действие будет вызвано программно).
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">Добавить пользователя</button>
</Form>
{action.value?.success && (
// Если действие выполнено успешно, свойство `action.value` будет содержать возвращаемое значение действия.
<p>Пользователь {action.value.userID} успешно добавлен</p>
)}
</>
);
});
Поскольку действия не выполняются во время рендера, они могут иметь побочные эффекты, такие как запись в базу данных или отправка электронного письма. Действие выполняется только при явном вызове.
<Form/>
Использование действий с компонентом Лучший способ вызвать действие - использовать компонент <Form/>
, экспортированный в @builder.io/qwik-city
.
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">Добавить пользователя</button>
{action.value?.success && <p>Пользователь успешно добавлен</p>}
</Form>
);
});
Под капотом компонент <Form />
использует собственный элемент HTML <form>
, поэтому он будет работать без JavaScript.
Когда JS включен, компонент <Form />
будет перехватывать отправку формы и запускать действие в режиме SPA, что позволяет полноценно использовать весь опыт SPA.
Это сделано для уточнения того, что сервер повторно рендерит страницу и всё заново выполняет, поэтому, если у вас есть routeLoader$, он тоже будет выполнен.
Сложные формы могут быть созданы с помощью точечной нотации.
Программное использование действий
Действия могут быть запущены программно с помощью метода action.submit()
, т.е. компонент <Form />
не обязателен. Вы можете запустить действие по нажатию кнопки или при наступлении любого другого события, точно так же, как вы бы делали это с помощью функции.
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);
}}
>
Добавить пользователя
</button>
{action.value?.success && <p>Пользователь успешно добавлен</p>}
</section>
);
});
В приведённом выше примере действие addUser
срабатывает, когда пользователь нажимает на кнопку. Метод action.submit()
возвращает Promise
, который разрешается, когда действие выполнено.
Действия с обработчиками событий
Мы можем использовать обработчик события onSubmitCompleted$
после того, как действие успешно выполнено и возвращает некоторые данные. Это особенно полезно, когда после успешного завершения действия необходимо выполнить дополнительные задачи, такие как сброс элементов пользовательского интерфейса или обновление состояния приложения.
Вот пример обработчика onSubmitCompleted$
, используемого для редактирования элемента в компоненте EditForm приложения Todo.
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}`}
/>
{/* Отправляет item.id с данными формы при отправке. */}
<input type="hidden" name="id" value={item.id} />
<button type="submit">
Отправить
</button>
</Form>
<div>
<button onClick$={() => (editingIdSignal.value = '')}>
Отмена
</button>
</div>
</div>
);
}
);
export default EditForm;
В этом примере onSubmitCompleted$
используется для сброса значения editIdSignal в пустую строку после успешного завершения отправки формы. Это позволяет приложению обновить свое состояние и вернуться к виду по умолчанию.
Валидация и безопасность типов
Qwik поставляется со встроенной поддержкой Zod, проверкой схем на основе TypeScript, которые можно использовать непосредственно в действиях с помощью функции zod$()
.
Действия + Zod позволяют создавать безопасные формы, в которых данные проверяются на стороне сервера перед выполнением действия.
import { component$ } from '@builder.io/qwik';
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city';
export const useAddUser = routeAction$(
async (user) => {
// `user` строго типизирован: { firstName: string, lastName: string }
const userID = await db.users.add({
firstName: user.firstName,
lastName: user.lastName,
});
return {
success: true,
userID,
};
},
// Zod-схема используется для проверки того, что FormData включает `firstName` и `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">Добавить пользователя</button>
</Form>
{action.value?.success && (
<p>Пользователь {action.value.userID} успешно добавлен</p>
)}
</>
);
});
При отправке данных в routeAction()
, данные проверяются на соответствие схеме Zod. Если данные недействительны, действие поместит ошибку валидации в свойство routeAction.value
.
Пожалуйста, обратитесь к документации Zod для получения дополнительной информации о том, как использовать схемы валидации Zod.
Расширенная проверка на основе событий
Конструктор zod$
может принимать функцию, поскольку первым аргументом является сам zod, поэтому вы можете использовать его непосредственно для построения схемы. Вторым параметром является RequestEvent для построения схемы zod на основе событий. Особенно в сочетании с refine
и superDefine
в zod, единственным пределом является ваше воображение.
export const useAddUser = routeAction$(
async (user) => {
// "user" по-прежнему сильно типизирован, но firstname
// теперь является необязательным: { firstName? string | undefined, LastName: string }
const userID = await db.users.add({
firstName: user.firstName,
lastName: user.lastName,
});
return {
success: true,
userID,
};
},
// Схема Zod используется для проверки того, что FormData включает "firstName" и "LastName".
zod$((z, ev) => {
// Имя является необязательным, если url содержит параметр запроса "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$
имеют доступ к объекту RequestEvent
, который содержит информацию о текущем HTTP-запросе и ответе.
Это позволяет действиям получать доступ к заголовкам запроса, кукам, url-адресу и переменным окружения в функции routeAction$
.
import { routeAction$ } from '@builder.io/qwik-city';
// Вторым аргументом действия является объект `RequestEvent`.
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` типизирован как { name: string }
const userID = await db.users.add(user);
if (!userID) {
return fail(500, {
message: 'Пользователь не может быть добавлен',
});
}
return {
userID,
};
},
zod$({
name: z.string(),
})
);
Ошибки сохраняются в свойстве action.value
, как и значение успешного завершения действия. Если действие не удалось, то свойство action.value.failed
устанавливается в true
. Кроме того, сообщения о сбоях могут быть найдены в объекте fieldErrors
в соответствии со свойствами, определёнными в вашей Zod-схеме.
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">Добавить пользователя</button>
{action.value?.failed && <p>{action.value.fieldErrors.name}</p>}
{action.value?.userID && <p>Пользователь успешно добавлен</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) => {
// обработка действия...
});
export default component$(() => {
const action = useAddUser();
return (
<Form action={action}>
<input name="name" value={action.formData?.get('name')} />
<button type="submit">Добавить пользователя</button>
</Form>
);
});
Особенно полезна функция action.formData
, позволяющая сохранять данные формы, заполненные пользователем, даже при обновлении страницы, что дает возможность полноценно работать со SPA даже при отключенном JS.
Маршрутные и глобальные действия
Действия могут быть объявлены с помощью routeAction$()
или globalAction$()
, экспортируемых из @builder.io/qwik-city
, единственное различие между ними заключается в том, что routeAction$()
привязано к маршруту, в то время как globalAction$()
доступно глобально для всего приложения.
Мы рекомендуем начинать с routeAction$()
и использовать globalAction$()
только тогда, когда вам нужно разделить действие между несколькими маршрутами, или если вы хотите использовать действие в компоненте, который не является маршрутом.
routeAction$()
routeAction$()
может быть объявлен только внутри папки src/routes
, в файле layout.tsx
или index.tsx
, и он ДОЛЖЕН быть экспортирован, как и routeLoader$()
. Поскольку routeAction$()
доступны только в пределах объявленного маршрута, их рекомендуется использовать, когда действие должно получить доступ к каким-либо пользовательским данным, или это защищённый маршрут. Думайте о нём, как о приватном действии.
Если вы хотите управлять общими многократно используемыми routeLoaders$, необходимо, чтобы эта функция была реэкспортирована из файла 'layout.tsx' или 'index.tsx' существующего маршрута, иначе она не будет запущена или выбросит исключение. Для получения дополнительной информации обратитесь к разделу рецептов.
import { routeAction$ } from '@builder.io/qwik-city';
export const useChangePassword = routeAction$((data) => {
// ...
});
globalAction$()
globalAction$()
может быть объявлен в любом месте папки src
. Поскольку globalAction$()
являются глобально доступными, они рекомендуются, когда действие должно быть общим для нескольких маршрутов, или когда действие не должно обращаться к пользовательским данным. Например, действие useLogin
, которое регистрирует пользователя. Думайте о нём, как о публичном действии.
import { globalAction$ } from '@builder.io/qwik-city';
export const useLogin = globalAction$((data) => {
// ...
});