Handle submission

Now your first form is almost ready. There is only one little thing missing and that is the data processing when the form is submitted.

Form actions

To make your form work even if JavaScript is disabled in your user's browser, we recommend using actions. Optionally, you can also perform the data processing on the client with onSubmit$ or use both in parallel.

You can find more about progressively enhanced forms in this guide.

Create an action

To create an action, you don't use Qwik's routeAction$ or globalAction$, but instead our formAction$ which builds on globalAction$ and adds additional functionality. The code of an action is only executed server-side. This has the advantage that the validation cannot be manipulated and you can communicate directly with backend services like a database.

You can then pass your action to the useForm hook, similar to how it works with the loader. More about this in the following code example.

Code example

import { $, component$, QRL } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import {
  type InitialValues,
} from '@modular-forms/qwik';
import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(
    v.nonEmpty('Please enter your email.'),
    v.email('The email address is badly formatted.')
  password: v.pipe(
    v.nonEmpty('Please enter your password.'),
    v.minLength(8, 'You password must have 8 characters or more.')

type LoginForm = v.InferInput<typeof LoginSchema>;

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
  email: '',
  password: '',

export const useFormAction = formAction$<LoginForm>((values) => {
  // Runs on server
}, valiForm$(LoginSchema));

export default component$(() => {
  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
    action: useFormAction(),
    validate: valiForm$(LoginSchema),

  const handleSubmit: QRL<SubmitHandler<LoginForm>> = $((values, event) => {
    // Runs on client

  return <Form onSubmit$={handleSubmit}></Form>;

Prevent default

When the form is submitted, event.preventDefault() is executed by default to prevent the window from reloading so that the values can also be processed directly in the browser and the state of the form is preserved.

Loading animation

While the form is being submitted, you can use loginForm.submitting to display a loading animation and disable the submit button.

Only dirty values

Using the shouldDirty property of the Form component, you have the option to have the form return only modified values. This reduces network traffic, for example if you only want to update values that have changed in your database.

Return custom data

If you process form values with actions, you may wonder how to return individual data to your component. For example, the ID of a user after login.

To do this, first add a second generic to formAction$ and useForm that defines your individual data and makes it type safe. Then you can return this data in formAction$ via the data key to access it afterwards with loginForm.response.data in your component.

type ResponseData = {
  userId: string;

export const useFormAction = formAction$<LoginForm, ResponseData>(
  async (values) => {
    const userId = await loginUser(values);
    return {
      status: 'success',
      message: 'You are now logged in.',
      data: { userId },