# React Hook Form (/docs/react-hook-form)



React Hook Form owns the form state, and Luke UI renders the controls.

## Initialise the form [#initialise-the-form]

Call `useForm` with `defaultValues` and a resolver, and keep the result as `form`. React Hook Form
takes the form's value types from the Zod schema, so no hand-written type repeats it.

```tsx
const schema = z.object({
	email: z.email('Enter an email address in the form you@example.com.'),
	name: z.string().min(1, 'Enter your name.'),
});

const form = useForm({
	defaultValues: { email: '', name: '' },
	resolver: zodResolver(schema),
});
```

## Integrate components [#integrate-components]

Wrap each control in `Controller`. Its `render` prop hands you `field` and `fieldState`. Pass
`field.value`, `field.onChange`, and `field.onBlur` to the control, and give `field.ref` to
`inputRef`.

apps/docs/src/examples/forms/react-hook-form.tsx

```tsx
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@luke-ui/react/button';
import { Cluster } from '@luke-ui/react/cluster';
import { Stack } from '@luke-ui/react/stack';
import { Text } from '@luke-ui/react/text';
import { TextField } from '@luke-ui/react/text-field';
import { Controller, useForm } from 'react-hook-form';
import * as z from 'zod';

const schema = z.object({
	email: z.email('Enter an email address in the form you@example.com.'),
	name: z.string().min(1, 'Enter your name.'),
});

export default () => {
	const form = useForm({
		defaultValues: { email: '', name: '' },
		resolver: zodResolver(schema),
	});

	return (
		<Stack gap="sp16" maxInlineSize="20rem">
			<form onSubmit={form.handleSubmit(() => undefined)}>
				<Stack gap="sp16">
					<Controller
						control={form.control}
						name="name"
						render={({ field, fieldState }) => (
							<Stack minBlockSize="5.5rem">
								<TextField
									errorMessage={fieldState.error?.message}
									inputRef={field.ref}
									label="Name"
									onBlur={field.onBlur}
									onChange={field.onChange}
									validationBehavior="aria"
									value={field.value}
								/>
							</Stack>
						)}
					/>
					<Controller
						control={form.control}
						name="email"
						render={({ field, fieldState }) => (
							<Stack minBlockSize="5.5rem">
								<TextField
									errorMessage={fieldState.error?.message}
									inputRef={field.ref}
									label="Email"
									onBlur={field.onBlur}
									onChange={field.onChange}
									validationBehavior="aria"
									value={field.value}
								/>
							</Stack>
						)}
					/>
					<Cluster>
						<Button type="submit">Create account</Button>
					</Cluster>
				</Stack>
			</form>
			<Stack minBlockSize="1.5rem">
				<Text elementType="p" role="status">
					{form.formState.isSubmitSuccessful ? `Submitted: ${form.getValues('name')}` : '\u00a0'}
				</Text>
			</Stack>
		</Stack>
	);
};
```

A checkbox reads its value from `isSelected`.

apps/docs/src/examples/forms/react-hook-form-checkbox.tsx

```tsx
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@luke-ui/react/button';
import { Checkbox } from '@luke-ui/react/checkbox';
import { Cluster } from '@luke-ui/react/cluster';
import { Stack } from '@luke-ui/react/stack';
import { Text } from '@luke-ui/react/text';
import { Controller, useForm } from 'react-hook-form';
import * as z from 'zod';

const schema = z.object({
	terms: z.boolean().refine((accepted) => accepted, {
		error: 'Accept the terms of service before you continue.',
	}),
});

export default () => {
	const form = useForm({
		defaultValues: { terms: false },
		resolver: zodResolver(schema),
	});

	return (
		<Stack gap="sp16" maxInlineSize="20rem">
			<form onSubmit={form.handleSubmit(() => undefined)}>
				<Stack gap="sp16">
					<Controller
						control={form.control}
						name="terms"
						render={({ field, fieldState }) => (
							<Stack minBlockSize="4.5rem">
								<Checkbox
									errorMessage={fieldState.error?.message}
									inputRef={field.ref}
									isSelected={field.value}
									onBlur={field.onBlur}
									onChange={field.onChange}
									validationBehavior="aria"
								>
									I accept the terms of service
								</Checkbox>
							</Stack>
						)}
					/>
					<Cluster>
						<Button type="submit">Continue</Button>
					</Cluster>
				</Stack>
			</form>
			<Stack minBlockSize="1.5rem">
				<Text elementType="p" role="status">
					{form.formState.isSubmitSuccessful ? 'Terms accepted.' : '\u00a0'}
				</Text>
			</Stack>
		</Stack>
	);
};
```

`TextField` and `Checkbox` render a label, description, and error message around the control, so
`inputRef` is what reaches the input underneath. A primitive that renders the control itself, such
as `ComboboxInput`, takes `field.ref` on `ref`.

## Validation [#validation]

Read the message from `fieldState.error` and pass it to `errorMessage`. The message marks the field
invalid.

```tsx
<Controller
	control={form.control}
	name="email"
	render={({ field, fieldState }) => (
		<TextField
			errorMessage={fieldState.error?.message}
			inputRef={field.ref}
			label="Email"
			onBlur={field.onBlur}
			onChange={field.onChange}
			validationBehavior="aria"
			value={field.value}
		/>
	)}
/>
```

Set `validationBehavior="aria"` on every field a `Controller` wraps. Read
[Validation](/docs/validation#let-the-browser-validate) for why native behaviour blocks the submit
event before the library can run.

## Focus the first invalid field [#focus-the-first-invalid-field]

React Hook Form focuses the first invalid control after a failed submission, using the ref each
field registered. A field that never receives `field.ref` stays unfocused, and someone filling in
the form gets an error message without being taken to it.

Set `shouldFocusError: false` on `useForm` to turn this off.

## Submitting data [#submitting-data]

Wrap the submit handler in `form.handleSubmit`. It runs the schema first, then calls the handler
with the values.

```tsx
<form onSubmit={form.handleSubmit((values) => saveAccount(values))}>
```

## Continue learning [#continue-learning]

<Cards>
  <Card href="/docs/validation" title="Validation">
    Choose validationBehavior and write the error message.
  </Card>

  <Card href="/docs/forms" title="Forms">
    Build the same form with native submit and reset.
  </Card>

  <Card href="/docs/tanstack-form" title="TanStack Form">
    See the same pattern with form.Field.
  </Card>
</Cards>
