Integration / Remix

Remix

Here is a login form example integrating with Remix. You can find the full example here.

1import { getFormProps, getInputProps, useForm } from '@conform-to/react';
2import { parseWithZod } from '@conform-to/zod';
3import type { ActionArgs } from '@remix-run/node';
4import { json, redirect } from '@remix-run/node';
5import { Form, useActionData } from '@remix-run/react';
6import { z } from 'zod';
7
8const schema = z.object({
9  email: z.string().email(),
10  password: z.string(),
11  remember: z.boolean().optional(),
12});
13
14export async function action({ request }: ActionArgs) {
15  const formData = await request.formData();
16  const submission = parseWithZod(formData, { schema });
17
18  if (submission.status !== 'success') {
19    return json(submission.reply());
20  }
21
22  // ...
23}
24
25export default function Login() {
26  // Last submission returned by the server
27  const lastResult = useActionData<typeof action>();
28  const [form, fields] = useForm({
29    // Sync the result of last submission
30    lastResult,
31
32    // Reuse the validation logic on the client
33    onValidate({ formData }) {
34      return parseWithZod(formData, { schema });
35    },
36
37    // Validate the form on blur event triggered
38    shouldValidate: 'onBlur',
39    shouldRevalidate: 'onInput',
40  });
41
42  return (
43    <Form method="post" id={form.id} onSubmit={form.onSubmit} noValidate>
44      <div>
45        <label>Email</label>
46        <input
47          type="email"
48          key={fields.email.key}
49          name={fields.email.name}
50          defaultValue={fields.email.initialValue}
51        />
52        <div>{fields.email.errors}</div>
53      </div>
54      <div>
55        <label>Password</label>
56        <input
57          type="password"
58          key={fields.password.key}
59          name={fields.password.name}
60          defaultValue={fields.password.initialValue}
61        />
62        <div>{fields.password.errors}</div>
63      </div>
64      <label>
65        <div>
66          <span>Remember me</span>
67          <input
68            type="checkbox"
69            key={fields.remember.key}
70            name={fields.remember.name}
71            defaultChecked={fields.remember.initialValue === 'on'}
72          />
73        </div>
74      </label>
75      <hr />
76      <button>Login</button>
77    </Form>
78  );
79}

#Tips

The default value might be out of sync if you reset the form from the action

If the default value of the form comes from the loader and you are trying to reset the form on the action, there is a chance you will see the form reset to the previous default value. As Conform will reset the form the moment action data is updated while Remix is still revalidating the loader data. To fix this, you can wait for the state to be idle (e.g. navigation.state or fetcher.state) before passing the lastResult to Conform like this:

1export default function Example() {
2  const { defaultValue } = useLoaderData<typeof loader>();
3  const lastResult = useActionData<typeof action>();
4  const navigation = useNavigation();
5  const [form, fields] = useForm({
6    // If the default value comes from loader
7    defaultValue,
8
9    // Sync the result of last submission only when the state is idle
10    lastResult: navigation.state === 'idle' ? lastResult : null,
11
12    // or, if you are using a fetcher:
13    // lastResult: fetcher.state === 'idle' ? lastResult : null,
14
15    // ...
16  });
17
18  // ...
19}