routeAction$()

Действия позволяют обрабатывать отправленные формы, позволяя выполнять побочные эффекты, такие как запись в базу данных или отправка электронного письма.

Действия также могут возвращать данные обратно клиенту/браузеру, позволяя соответствующим образом обновлять пользовательский интерфейс, т.е. отображение сообщения об успехе после отправки формы.

Действия могут быть объявлены с помощью routeAction$() или globalAction$(), экспортируемых из @builder.io/qwik-city.

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) => {
  // Этот код будет выполняться на сервере только тогда, когда пользователь отправит форму (или когда действие будет вызвано программно).
  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.

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">Добавить пользователя</button>
      {action.value?.success && <p>Пользователь успешно добавлен</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);
        }}
      >
        Добавить пользователя
      </button>
      {action.value?.success && <p>Пользователь успешно добавлен</p>}
    </section>
  );
});

В приведённом выше примере действие addUser срабатывает, когда пользователь нажимает на кнопку. Метод action.submit() возвращает Promise, который разрешается, когда действие выполнено.

Действия с обработчиками событий

Мы можем использовать обработчик события onSubmitCompleted$ после того, как действие успешно выполнено и возвращает некоторые данные. Это особенно полезно, когда после успешного завершения действия необходимо выполнить дополнительные задачи, такие как сброс элементов пользовательского интерфейса или обновление состояния приложения.

Вот пример обработчика onSubmitCompleted$, используемого для редактирования элемента в компоненте EditForm приложения Todo.

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}`}
          />
          {/* Отправляет 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 позволяют создавать безопасные формы, в которых данные проверяются на стороне сервера перед выполнением действия.

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) => {
    // `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$.

src/routes/product/[user]/index.tsx
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' существующего маршрута, иначе она не будет запущена или выбросит исключение. Для получения дополнительной информации обратитесь к разделу рецептов.

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