Skip to main content

Forms

Maybern uses React Hook Form with Yup/Zod schemas for form handling and validation.

Basic Form

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  email: yup.string().email().required(),
  amount: yup.number().positive().required(),
});

type FormData = yup.InferType<typeof schema>;

function MyForm() {
  const methods = useForm<FormData>({
    resolver: yupResolver(schema),
    defaultValues: {
      name: "",
      email: "",
      amount: 0,
    },
  });
  
  const onSubmit = async (data: FormData) => {
    await saveData(data);
  };
  
  return (
    <Form methods={methods} onSubmit={onSubmit}>
      <FormField name="name" label="Name" required>
        <FormInput name="name" />
      </FormField>
      
      <FormField name="email" label="Email" required>
        <FormInput name="email" type="email" />
      </FormField>
      
      <FormField name="amount" label="Amount" required>
        <FormNumberInput name="amount" prefix="$" />
      </FormField>
      
      <SubmitButton isLoading={methods.formState.isSubmitting}>
        Save
      </SubmitButton>
    </Form>
  );
}

Validation Schemas

Yup Schema

import * as yup from "yup";

export const fundFamilySchema = yup.object({
  name: yup.string()
    .required("Name is required")
    .max(100, "Name too long"),
  shortName: yup.string()
    .max(20)
    .nullable(),
  effectiveDate: yup.date()
    .required()
    .max(new Date(), "Cannot be in the future"),
  commitment: yup.number()
    .positive("Must be positive")
    .required(),
});

Zod Schema

import { z } from "zod";

export const fundFamilySchema = z.object({
  name: z.string()
    .min(1, "Name is required")
    .max(100),
  shortName: z.string()
    .max(20)
    .nullable(),
  effectiveDate: z.date()
    .max(new Date()),
  commitment: z.number()
    .positive(),
});

type FormData = z.infer<typeof fundFamilySchema>;

Form Fields

Standard Inputs

// Text
<FormInput name="name" placeholder="Enter name" />

// Password
<FormInput name="password" type="password" />

// Textarea
<FormTextarea name="description" rows={4} />

Number Inputs

// Basic number
<FormNumberInput name="count" />

// Money
<FormMoneyInput name="amount" currency="USD" />

// Percentage
<FormPercentInput name="rate" />

Selection Inputs

// Select
<FormSelect name="status">
  <option value="active">Active</option>
  <option value="inactive">Inactive</option>
</FormSelect>

// Multi-select
<FormMultiSelect
  name="tags"
  options={tagOptions}
/>

// Radio group
<FormRadioGroup name="type" options={typeOptions} />

// Checkbox
<FormCheckbox name="confirmed">
  I confirm this is correct
</FormCheckbox>

Date Inputs

// Date picker
<FormDatePicker name="effectiveDate" />

// Date range
<FormDateRangePicker
  startName="startDate"
  endName="endDate"
/>

Error Handling

Field Errors

<FormField
  name="amount"
  label="Amount"
  error={errors.amount?.message}
>
  <FormNumberInput name="amount" />
</FormField>

Server Errors

const onSubmit = async (data: FormData) => {
  try {
    await saveData(data);
  } catch (error) {
    if (error.response?.data?.errors) {
      // Set field-specific errors
      Object.entries(error.response.data.errors).forEach(([field, message]) => {
        methods.setError(field, { message });
      });
    } else {
      // Set form-level error
      methods.setError("root", { message: "Something went wrong" });
    }
  }
};

Form Patterns

Edit Form with Initial Data

function EditForm({ initialData }: { initialData: FundFamily }) {
  const methods = useForm({
    resolver: yupResolver(schema),
    defaultValues: initialData,
  });
  
  // Reset when data changes
  useEffect(() => {
    methods.reset(initialData);
  }, [initialData]);
  
  return <Form methods={methods}>...</Form>;
}

Conditional Fields

function ConditionalForm() {
  const methods = useForm();
  const hasOverride = methods.watch("hasOverride");
  
  return (
    <Form methods={methods}>
      <FormCheckbox name="hasOverride">
        Use custom value
      </FormCheckbox>
      
      {hasOverride && (
        <FormNumberInput name="customValue" />
      )}
    </Form>
  );
}

Field Arrays

import { useFieldArray } from "react-hook-form";

function DynamicForm() {
  const methods = useForm();
  const { fields, append, remove } = useFieldArray({
    control: methods.control,
    name: "items",
  });
  
  return (
    <Form methods={methods}>
      {fields.map((field, index) => (
        <HStack key={field.id}>
          <FormInput name={`items.${index}.name`} />
          <IconButton onClick={() => remove(index)} />
        </HStack>
      ))}
      <Button onClick={() => append({ name: "" })}>
        Add Item
      </Button>
    </Form>
  );
}

Best Practices

Create validation schemas before building forms. They serve as documentation and enforce consistency.
Use yup.InferType or z.infer to derive form types from schemas.
Prefer React Hook Form’s uncontrolled approach. Use watch sparingly for dependent fields.
Always show validation errors near the relevant field. Use FormField wrapper for consistent error display.