Made in Builder.io

Upcoming webinar with Figma: Design to Code in 80% Less Time

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Talk to Us
Platform
Developers
Talk to Us

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Type Safe Forms in Qwik with Modular Forms

April 11, 2023

Written By Yoav Ganbar

As Qwik has reached RC status, let’s explore how Modular Forms with Qwik can enhance your developer experience while ensuring both client and server type safety.

Let’s dig in!

Server-side type safety with Zod

If you have been paying attention to all the TypeScript nerds lately, you’ve probably heard of Zod.

And I’m not talking about the villain from Superman 2 (which appeared also in more recent movies from the franchise).

In case you haven’t, Zod is a TypeScript-first schema declaration and validation library.

Zod is all about making your life easier as a developer. The whole point is to get rid of all that boring type declaration stuff and validation.

Basically, you just gotta declare your validator one freakin' time, and boom! Zod takes care of the rest by automatically figuring out the TypeScript type.

Plus, you can mix and match simple types to make more complex data structures like it ain't no thang.

Bonus points:

  • Works with Vanilla JavaScript
  • No dependencies
  • Small bundle footprint (13.1kB according to Bundleaphobia, although the site advertises 8k)

What does this have to do with Qwik? Glad you asked (patting my own back).

Zod, which has first-class support inside Qwik’s routeAction$() function, can handle form validation server-side.

Take this code as an example:

// FILE: src/routes/index.tsx

import { routeAction$ } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(async (user) => {
  // `user` is typed Record<string, any>
  const userID = await db.users.add(user);
  return {
    success: true,
    userID,
  };
}); 

Qwik does a decent job of giving us a type of Record<string, any>, but that is not ideal. There’s no validation that the user payload actually has what is needed to create a user inside our database.

This is where Zod comes into play:

// FILE: src/routes/index.tsx

import { routeAction$, zod$, z } from '@builder.io/qwik-city';
 
export const useAddUser = routeAction$(
  async (user) => {
    // `user` is typed { name: string }
    const userID = await db.users.add(user);
    return {
      success: true,
      userID,
    };
  },
  zod$({
    name: z.string(),
  })
);

By adding the zod$ function and passing in our validation scheme with the z primitive, we can get the correct type for the user object, as well as server-side validation.

This means that if we accidentally pass a user object (or an attacker tries something fishy) that doesn’t have the name property, the server will throw an error.

For more information about handling errors in Qwik routeAction, check out the Qwik Action Failures documentation.

As in the above, MPA (Multi-Page Application) form handling on the server is pretty nice. However, this is still not amazing UX.

For a user to understand that something wrong has happened, such as having entered the wrong type of input, they still need to wait for a server response.

That’s a frustrating experience if you ask me.

There’s a reason the React ecosystem has a slew of libraries that handle forms. To create a good client-side user experience, there’s a lot of boilerplate you’d need to write and edge cases that you need to handle.

Notable mentions are: Formik, React Hook Form, and React Final Form, which have made writing complex forms much easier.

Modular Forms is a form library for Qwik and SolidJS created by Fabian Hiller.

Fabian created a complex form for his SaaS business in 2018, but found that manually handling all the form validation was tiring and prone to errors. He decided to create a useForm hook to offload the repetitive code and make it reusable.

However, he was unsatisfied with the development experience and tested different form libraries before creating his own. Every decision in the library has a well-thought-out reason, as he’s listed on the library site.

Since Qwik already uses Zod, Modular Forms supports defining the form values as a Zod schema.

Let’s create a minimal login form using this approach:

// FILE: src/routes/modular-forms/index.tsx

import { routeLoader$, z } from '@builder.io/qwik-city';
import { InitialValues } from '@modular-forms/qwik';

const formSchema = z.object({
  email: z.string().nonempty(),
  password: z.string().min(8),
});

// Note: you can also use z.input 
// since Zod supports data transformation.
type LoginForm = z.infer<typeof formSchema>;

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

We have created a routeLoader$, which is data from the server with default values for our form.

Modular Forms need the default values to initialize the store of the form. Thanks to Qwik's resumability, this step can be done entirely on the server without runtime costs in the browser.

Now we can create our useForm hook to build out our form UI and client validations:

export default component$(() => {
  const [loginForm, { Form, Field, FieldArray }] = useForm<LoginForm>({
    loader: useFormLoader(),
  });
});

Note that the hook returns a tuple which we can name whatever we want à la React useState hook style.

As we can tell from the above, we get three components out of this hook that we can use to build out our UI: Form, Field, and FieldArray.

The Field component uses a headless approach, which means that it does not render any HTML, which gives you, as a developer, maximum flexibility.

All we have to do to add fields to our form is the following:

export default component$(() => {
  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
  });

  return (
    <Form>
      <Field name="email">
        {(field, props) => <input {...props} type="email" />}
      </Field>
      <Field name="password">
        {(field, props) => <input {...props} type="password" />}
      </Field>
      <button class="w-max" type="submit">
        Login
       </button>
    </Form>
  );
});

At this point, we will get no indication to any errors on the client, or on the server, as we do not have a routeAction$ to handle the form submission, nor any sort of client validation functions.

Modular Forms comes with its own validation functions, which you can use, but for the sake of this post, I will continue using Zod.

Tip: It’s important to note that validations in Modular Forms, except for server actions, happen on the browser.

To add Zod validations we need to change our schema a bit, like so:

const formSchema = z.object({
  email: z.string().nonempty(),
  password: z.string().min(8),
  email: z
    .string()
    .nonempty('please enter your email')
    .email('enter a valid email'),
  password: z
    .string()
    .min(1, 'please enter a password')
    .min(8, 'You password must have 8 characters or more.'),
});

The second argument to the Zod’s validation helper functions is just the error message that will be thrown.

In order to activate those validations, we need to pull in the zodForm$ adapter and add it as an argument to the useForm hook:

const [loginForm, { Form, Field }] = useForm<LoginForm>({
  loader: useFormLoader(),
  validate: zodForm$(formSchema),
});

Once we’ve added that, we need to display these errors. They will now be a part of the field argument. In case there is an error, the message will appear in the error property of that field.

To show the error, we can then add the following to our JSX:

export default component$(() => {
  const [_, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
    validate: zodForm$(formSchema),
  });

  return (
    <section class="p-4">
      <h1>Qwik Modular Forms</h1>
      <Form class="flex flex-col gap-2">
        <Field name="email">
          {(field, props) => (
            <>
              <input
                class="w-96"
                placeholder="enter email"
                {...props}
                type="email"
              />
              {field.error && <div>{field.error}</div>}
            </>
          )}
        </Field>
        <Field name="password">
          {(field, props) => (
            <>
              <input
                class={'w-96'}
                placeholder="enter password"
                {...props}
                type="password"
              />
              {field.error && <div>{field.error}</div>}
            </>
          )}
        </Field>
        <button class="w-max" type="submit">
          Login
        </button>
      </Form>
    </section>
  );
});

The result is that the error messages will render underneath the field, in case the field is invalid:

If you’ve been observant enough, you might have noticed that even though we have validation on the client, we still don’t actually submit it.

Now, to add this to our form, all we have to do is add a formAction$ function to our useForm:

// FILE: src/routes/modular-forms/index.tsx

// .... our previous code

export const useFormAction = formAction$<LoginForm>((values) => {
  // Runs on server
  console.log(values);
  // This validates the values on the server side.
  // And cannot be manipulated by an attacker. ✅ 
}, zodForm$(formSchema));

export default component$(() => {
  const [_, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
    validate: zodForm$(formSchema),
    action: useFormAction(),
  });

  // ... the rest of our previous code
}

To optionally process the form values client-side as well, we can add a function that is passed to the onSubmit$ property of the Form component.

export default component$(() => {
  // ... 

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

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

Something that I have not found clear was how you get the response from the server back on the client. Perhaps this is a needed feature to request, as this library is still in beta.

It’s just something that I got accustomed to with routeAction$, and I expected it to behave the same and give me a signal with the server response.

I asked Fabian, the creator of the library, and he helped me out.

As the library is both for SolidJS and Qwik, the intention is to keep a similar API. All that to say that in order to access the server response, all you need to do is return it with the FormActionResult signature.

To get our end to end types correct, we now need to add a return type as a second generic to our formAction$ call:

export const useFormAction = formAction$<LoginForm, LoginResponse>(
  async ({ email, password }) => {
    // Runs on server
    // simulating adding a user to the DB.
    const createdUserID = await db.users.add({ email, password })
    return {
      status: 'success',
      message: 'User added successfully',
      data: { createdUserID },
    };
  },
  zodForm$(formSchema)
);

Now we get that sweet auto complete in our client-side code:

Notice that we also get a loading state through the submitting property as well as the response data once the server responds:

This is more a feature of Qwik than the Modular Forms library, however, it’s worth noting. To show this off, all we need is to turn off JavaScript in your browser (Chrome devtools → open command palette → Disable JavaScript).

Once deactivated, the form will still work as below:

So, basically, if you're a developer and you want to build forms that are easy to use and maintain while ensuring type safety on both the client and server side, then you should check out the combination of Modular Forms and Qwik.

One of the cool things about Modular Forms is that it uses Zod for validation and schema definition on the server-side. This makes it super efficient and reliable. Also, it has built-in validation functions that you use to validate form data on the client-side. This helps to improve the user experience and prevent errors.

Now, Qwik is also pretty sweet because it has a progressively enhanced form feature. This means that even if JavaScript is off in the user's browser, the forms still work. So, everyone can use the forms, no matter what their browser settings are.

All in all, Modular Forms and Qwik make it super straightforward and safe for developers to build forms for their web applications and websites. So, if that's something you need to do, you should definitely give this combo a try!

Introducing Visual Copilot: convert Figma designs to code using your existing components in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Introducing Visual Copilot:

A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
AI9 MIN
How to Build AI Products That Don’t Flop
WRITTEN BYSteve Sewell
April 18, 2024
Web Development13 MIN
Convert Figma to Code with AI
WRITTEN BYVishwas Gopinath
April 18, 2024
Web Development8 MIN
Server-only Code in Next.js App Router
WRITTEN BYVishwas Gopinath
April 3, 2024