Forms & Validation

Jumbo React Vite uses React Hook Form 7 with Zod 4 for all form state and validation. The @jumbo/vendors/react-hook-form package provides a JumboForm context wrapper and a set of MUI field components that bind to RHF automatically.

The JumboForm pattern

JumboForm wraps RHF's FormProvider and optionally connects a Zod schema via zodResolver. All child Jumbo* field components read from this context — no manual register() calls needed.

tsx
import { JumboForm, JumboInput } from '@jumbo/vendors/react-hook-form';
import { z } from 'zod';
 
const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Enter a valid email'),
});
 
type FormInput = z.infer<typeof schema>;
 
export function ContactForm() {
  const handleSuccess = (data: FormInput) => {
    console.log('Submitted:', data);
  };
 
  return (
    <JumboForm<FormInput>
      onSuccess={handleSuccess}
      validationSchema={schema}
      defaultValues={{ name: '', email: '' }}
    >
      <JumboInput name='name' label='Full name' fullWidth sx={{ mb: 2 }} />
      <JumboInput name='email' label='Email' type='email' fullWidth sx={{ mb: 2 }} />
      <Button type='submit' variant='contained'>Submit</Button>
    </JumboForm>
  );
}

Field components

ComponentMUI baseUse case
JumboInputTextFieldText, email, password, number inputs
JumboOutlinedInputOutlinedInput + InputLabelInputs where you need the label outside the box
JumboCheckboxCheckbox + FormControlLabelBoolean toggles, consent checkboxes
JumboSelectSelect + InputLabel + FormControlDropdown selections from a fixed option set

All field components display inline validation error messages sourced from the Zod schema automatically — no manual helperText wiring is needed.

Building a complete form with validation

  1. Define the Zod schema

    Always define the schema first. The TypeScript type is inferred from it — never write the type manually.

    typescript
    // src/components/SignupForm/schema.ts
    import { z } from 'zod';
     
    export const signupSchema = z.object({
      firstName: z.string().min(1, 'First name is required'),
      lastName:  z.string().min(1, 'Last name is required'),
      email:     z.string().email('Enter a valid email address'),
      password:  z.string().min(8, 'Password must be at least 8 characters'),
      role:      z.enum(['admin', 'editor', 'viewer']),
      terms:     z.literal(true, {
        errorMap: () => ({ message: 'You must accept the terms' }),
      }),
    });
     
    export type SignupInput = z.infer<typeof signupSchema>;
  2. Build the form component

    tsx
    // src/components/SignupForm/SignupForm.tsx
    import {
      JumboCheckbox,
      JumboForm,
      JumboInput,
      JumboSelect,
    } from '@jumbo/vendors/react-hook-form';
    import { Button, Stack } from '@mui/material';
    import { signupSchema, type SignupInput } from './schema';
     
    const roleOptions = [
      { value: 'admin',  label: 'Admin' },
      { value: 'editor', label: 'Editor' },
      { value: 'viewer', label: 'Viewer' },
    ];
     
    export function SignupForm({ onSuccess }: { onSuccess: (data: SignupInput) => void }) {
      return (
        <JumboForm<SignupInput>
          onSuccess={onSuccess}
          validationSchema={signupSchema}
          defaultValues={{
            firstName: '', lastName: '', email: '',
            password: '', role: 'viewer', terms: false,
          }}
        >
          <Stack spacing={2}>
            <JumboInput name='firstName' label='First name' fullWidth />
            <JumboInput name='lastName'  label='Last name'  fullWidth />
            <JumboInput name='email'     label='Email'      type='email' fullWidth />
            <JumboInput name='password'  label='Password'   type='password' fullWidth />
            <JumboSelect name='role' label='Role' options={roleOptions} fullWidth />
            <JumboCheckbox name='terms' label='I accept the terms and conditions' />
            <Button type='submit' variant='contained' fullWidth>
              Create account
            </Button>
          </Stack>
        </JumboForm>
      );
    }
  3. Use the form in a page

    tsx
    // src/pages/auth/Signup1Page.tsx
    import { SignupForm } from '@/components/SignupForm/SignupForm';
    import { useNavigate } from 'react-router-dom';
     
    export function Signup1Page() {
      const navigate = useNavigate();
     
      const handleSuccess = async (data: SignupInput) => {
        await createUser(data);    // your API call
        navigate('/dashboards/misc');
      };
     
      return <SignupForm onSuccess={handleSuccess} />;
    }

Accessing form state outside JumboForm

JumboForm renders an RHF FormProvider. Use useFormContext() in any descendant to access the full RHF API:

tsx
import { useFormContext } from 'react-hook-form';
import type { SignupInput } from './schema';
 
function PasswordStrength() {
  const { watch } = useFormContext<SignupInput>();
  const password = watch('password');
  const strength = calculateStrength(password);
 
  return <LinearProgress variant='determinate' value={strength} />;
}