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
| Scenario | Recommendation |
|---|---|
| Standard login, signup, profile, or settings forms | Use JumboForm + @jumbo field components |
| Complex custom form layouts with conditional fields | Use RHF useForm directly with @hookform/resolvers/zod |
| Server Actions (form submission without JS) | Use RHF useForm + startTransition / Next.js action prop |
Basic JumboForm pattern
// 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):
// 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
// 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'),
}),
})Use z.infer<typeof schema> to derive your TypeScript type. This ensures the type stays in
sync with the schema and eliminates the need to maintain separate type definitions.
Multi-step forms
For multi-step flows, manage the active step in local state and render different JumboForm
sections:
'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
| Component | MUI basis | When to use |
|---|---|---|
JumboInput | TextField (filled) | Standard labeled text/email/number fields |
JumboOutlinedInput | OutlinedInput | Fields without a floating label |
JumboSelect | Select | Dropdowns; pass options as [{ value, label }] |
JumboCheckbox | Checkbox | Boolean toggles with a label |
JumboAvatarField | Custom | Avatar image upload with preview |
JumboColorPickerField | react-color | Color 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:
'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:
'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>
);
}