React Hook Form Integration

Davis form components work as both controlled and uncontrolled inputs, making them compatible with react-hook-form out of the box.


Installation

Install react-hook-form as a dependency. It is not bundled with Davis.

npm install react-hook-form

Basic Form

Use register() to connect Davis inputs directly to the form. Most uncontrolled inputs (Input, Textarea, Select, Checkbox, Switch) work with register() without any wrapper.

import { useForm } from 'react-hook-form';
import { Input, Textarea, Select, Checkbox, Switch, Button } from '@libretexts/davis-react';

type FormValues = {
  name: string;
  email: string;
  role: string;
  bio: string;
  newsletter: boolean;
  notifications: boolean;
};

export default function BasicForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>();

  const onSubmit = (data: FormValues) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
      <Input
        label="Name"
        {...register('name', { required: 'Name is required' })}
        error={!!errors.name}
        errorMessage={errors.name?.message}
      />
      <Input
        label="Email"
        type="email"
        {...register('email', { required: 'Email is required' })}
        error={!!errors.email}
        errorMessage={errors.email?.message}
      />
      <Select
        label="Role"
        options={[
          { value: 'engineer', label: 'Engineer' },
          { value: 'designer', label: 'Designer' },
          { value: 'manager', label: 'Manager' },
        ]}
        {...register('role', { required: 'Role is required' })}
        error={!!errors.role}
        errorMessage={errors.role?.message}
      />
      <Textarea
        label="Bio"
        {...register('bio')}
      />
      <Checkbox label="Subscribe to newsletter" {...register('newsletter')} />
      <Switch label="Enable notifications" {...register('notifications')} />
      <Button type="submit">Submit</Button>
    </form>
  );
}

Displaying Errors

Davis form inputs accept error (boolean) and errorMessage (string) props. Map formState.errors to these props as shown above.

import { useForm } from 'react-hook-form';
import { Input } from '@libretexts/davis-react';

export default function ErrorExample() {
  const { register, handleSubmit, formState: { errors } } = useForm<{ email: string }>();

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <Input
        label="Email"
        type="email"
        {...register('email', {
          required: 'Email is required',
          pattern: {
            value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: 'Enter a valid email address',
          },
        })}
        error={!!errors.email}
        errorMessage={errors.email?.message}
      />
    </form>
  );
}

Controlled Components

Some components manage their own state and need react-hook-form's Controller wrapper: Combobox, NumberInput, and RadioGroup.

import { useForm, Controller } from 'react-hook-form';
import { Combobox, NumberInput, RadioGroup, Button } from '@libretexts/davis-react';

const FRAMEWORK_OPTIONS = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
];

type FormValues = {
  framework: { value: string; label: string } | null;
  quantity: number;
  priority: string;
};

export default function ControlledForm() {
  const { control, handleSubmit } = useForm<FormValues>({
    defaultValues: { quantity: 1, priority: 'medium' },
  });

  return (
    <form onSubmit={handleSubmit(console.log)} className="flex flex-col gap-4">
      <Controller
        name="framework"
        control={control}
        rules={{ required: 'Please select a framework' }}
        render={({ field, fieldState }) => (
          <Combobox
            label="Framework"
            options={FRAMEWORK_OPTIONS}
            value={field.value}
            onChange={field.onChange}
            error={!!fieldState.error}
            errorMessage={fieldState.error?.message}
          />
        )}
      />
      <Controller
        name="quantity"
        control={control}
        render={({ field }) => (
          <NumberInput
            label="Quantity"
            value={field.value}
            onChange={field.onChange}
            min={1}
            max={99}
          />
        )}
      />
      <Controller
        name="priority"
        control={control}
        render={({ field }) => (
          <RadioGroup
            label="Priority"
            options={[
              { value: 'low', label: 'Low' },
              { value: 'medium', label: 'Medium' },
              { value: 'high', label: 'High' },
            ]}
            value={field.value}
            onChange={field.onChange}
          />
        )}
      />
      <Button type="submit">Submit</Button>
    </form>
  );
}

FormSection Composition

Use FormSection to organize long forms into logical groups. Each section can independently render validation errors.

import { useForm } from 'react-hook-form';
import { FormSection, Input, Select, Textarea, Stack, Button } from '@libretexts/davis-react';

type FormValues = {
  firstName: string;
  lastName: string;
  email: string;
  department: string;
  bio: string;
};

export default function MultiSectionForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>();

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <Stack gap="lg">
        <FormSection
          title="Personal Information"
          description="Your name and contact details."
        >
          <Stack gap="md">
            <Input
              label="First name"
              {...register('firstName', { required: 'First name is required' })}
              error={!!errors.firstName}
              errorMessage={errors.firstName?.message}
            />
            <Input
              label="Last name"
              {...register('lastName', { required: 'Last name is required' })}
              error={!!errors.lastName}
              errorMessage={errors.lastName?.message}
            />
            <Input
              label="Email"
              type="email"
              {...register('email', { required: 'Email is required' })}
              error={!!errors.email}
              errorMessage={errors.email?.message}
            />
          </Stack>
        </FormSection>

        <FormSection
          title="Work Details"
          description="Your role and department."
        >
          <Stack gap="md">
            <Select
              label="Department"
              options={[
                { value: 'engineering', label: 'Engineering' },
                { value: 'design', label: 'Design' },
                { value: 'product', label: 'Product' },
              ]}
              {...register('department')}
            />
            <Textarea
              label="Bio"
              {...register('bio')}
              helperText="Tell us a bit about yourself."
            />
          </Stack>
        </FormSection>

        <div className="flex justify-end gap-3">
          <Button variant="outline" type="button">Cancel</Button>
          <Button type="submit">Save Changes</Button>
        </div>
      </Stack>
    </form>
  );
}

Validation with Zod

Use @hookform/resolvers with Zod for schema-based validation. This keeps validation logic separate from the UI.

npm install @hookform/resolvers zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Input, Select, Button } from '@libretexts/davis-react';

const schema = z.object({
  name: z.string().min(2, 'Name must be at least 2 characters'),
  email: z.string().email('Enter a valid email address'),
  role: z.string().min(1, 'Please select a role'),
});

type FormValues = z.infer<typeof schema>;

export default function ZodForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });

  return (
    <form onSubmit={handleSubmit(console.log)} className="flex flex-col gap-4">
      <Input
        label="Name"
        {...register('name')}
        error={!!errors.name}
        errorMessage={errors.name?.message}
      />
      <Input
        label="Email"
        type="email"
        {...register('email')}
        error={!!errors.email}
        errorMessage={errors.email?.message}
      />
      <Select
        label="Role"
        options={[
          { value: 'engineer', label: 'Engineer' },
          { value: 'designer', label: 'Designer' },
        ]}
        {...register('role')}
        error={!!errors.role}
        errorMessage={errors.role?.message}
      />
      <Button type="submit">Submit</Button>
    </form>
  );
}