Forms & Validation

Forms in Jumbo React Next are built with React Hook Form 7 and validated with Zod 4 schemas via @hookform/resolvers. The @jumbo/vendors/react-hook-form package wraps this combination into the JumboForm component and a set of pre-built field components that handle registration, error display, and styling automatically.

When to use JumboForm vs useForm

ScenarioRecommendation
Standard login, signup, profile, or settings formsUse JumboForm + @jumbo field components
Complex custom form layouts with conditional fieldsUse RHF useForm directly with @hookform/resolvers/zod
Server Actions (form submission without JS)Use RHF useForm + startTransition / Next.js action prop

Basic JumboForm pattern

tsx
// src/components/LoginForm/LoginForm.tsx
'use client';
 
import { JumboForm, JumboOutlinedInput } from '@jumbo/vendors/react-hook-form';
import { Button, Stack } from '@mui/material';
import { signIn } from 'next-auth/react';
import { validationSchema, type LoginFormValues } from './validation';
 
export function LoginForm() {
  const handleSubmit = async (data: LoginFormValues) => {
    const result = await signIn('credentials', {
      email:       data.email,
      password:    data.password,
      redirect:    true,
      callbackUrl: '/dashboards/misc',
    });
  };
 
  return (
    <JumboForm validationSchema={validationSchema} onSubmit={handleSubmit}>
      <Stack spacing={2}>
        <JumboOutlinedInput name="email"    label="Email"    type="email"    fullWidth />
        <JumboOutlinedInput name="password" label="Password" type="password" fullWidth />
        <Button type="submit" variant="contained" size="large" fullWidth>
          Sign In
        </Button>
      </Stack>
    </JumboForm>
  );
}

Zod schema patterns

Schemas live in a co-located validation.ts (or validation.tsx):

typescript
// src/components/LoginForm/validation.tsx
import { z } from 'zod';
 
export const validationSchema = z.object({
  email: z
    .string()
    .min(1, 'Email is required')
    .email('Enter a valid email address'),
  password: z
    .string()
    .min(6, 'Password must be at least 6 characters')
    .max(32, 'Password must not exceed 32 characters'),
});
 
export type LoginFormValues = z.infer<typeof validationSchema>;

Common Zod patterns

typescript
// Required string
z.string().min(1, 'This field is required')
 
// Email
z.string().email('Invalid email address')
 
// URL
z.string().url('Enter a valid URL')
 
// Number range
z.number().min(0).max(100)
 
// Optional field
z.string().optional()
 
// Nullable
z.string().nullable()
 
// Enum
z.enum(['admin', 'editor', 'viewer'])
 
// Object with nested validation
z.object({
  address: z.object({
    street: z.string().min(1),
    city:   z.string().min(1),
    zip:    z.string().regex(/^\d{5}$/, 'Enter a 5-digit ZIP code'),
  }),
})

Multi-step forms

For multi-step flows, manage the active step in local state and render different JumboForm sections:

tsx
'use client';
 
import { useState } from 'react';
import { JumboForm, JumboOutlinedInput } from '@jumbo/vendors/react-hook-form';
import { stepOneSchema, stepTwoSchema } from './validation';
 
export function MultiStepForm() {
  const [step, setStep] = useState(1);
 
  const handleStepOne = (data: StepOneValues) => {
    // persist step one data, advance to step two
    setStep(2);
  };
 
  if (step === 1) {
    return (
      <JumboForm validationSchema={stepOneSchema} onSubmit={handleStepOne}>
        <JumboOutlinedInput name="name"  label="Full Name" fullWidth />
        <JumboOutlinedInput name="email" label="Email"     fullWidth />
        <button type="submit">Next</button>
      </JumboForm>
    );
  }
 
  return (
    <JumboForm validationSchema={stepTwoSchema} onSubmit={handleSubmitFinal}>
      <JumboOutlinedInput name="password" label="Password" type="password" fullWidth />
      <button type="submit">Create Account</button>
    </JumboForm>
  );
}

Available field components

ComponentMUI basisWhen to use
JumboInputTextField (filled)Standard labeled text/email/number fields
JumboOutlinedInputOutlinedInputFields without a floating label
JumboSelectSelectDropdowns; pass options as [{ value, label }]
JumboCheckboxCheckboxBoolean toggles with a label
JumboAvatarFieldCustomAvatar image upload with preview
JumboColorPickerFieldreact-colorColor selection with a color swatch

All field components accept name (required, maps to the schema key) plus any props from their underlying MUI component.

Accessing form methods in child components

Use useJumboFormContext inside any component that is a descendant of JumboForm:

tsx
'use client';
 
import { useJumboFormContext } from '@jumbo/vendors/react-hook-form';
 
export function FormResetButton() {
  const { reset, formState: { isDirty } } = useJumboFormContext();
 
  return (
    <button type="button" disabled={!isDirty} onClick={() => reset()}>
      Reset
    </button>
  );
}

Using useForm directly

For forms where JumboForm does not fit (e.g. custom layouts), use RHF's useForm with Zod directly:

tsx
'use client';
 
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { TextField } from '@mui/material';
import { profileSchema, type ProfileFormValues } from './validation';
 
export function ProfileForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<ProfileFormValues>({
    resolver: zodResolver(profileSchema),
  });
 
  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <TextField
        {...register('bio')}
        label="Bio"
        error={!!errors.bio}
        helperText={errors.bio?.message}
        multiline
        rows={4}
        fullWidth
      />
      <button type="submit">Save</button>
    </form>
  );
}