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.
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
| Component | MUI base | Use case |
|---|---|---|
JumboInput | TextField | Text, email, password, number inputs |
JumboOutlinedInput | OutlinedInput + InputLabel | Inputs where you need the label outside the box |
JumboCheckbox | Checkbox + FormControlLabel | Boolean toggles, consent checkboxes |
JumboSelect | Select + InputLabel + FormControl | Dropdown 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
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>;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> ); }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:
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} />;
}Never use RHF's inline rules prop for validation. Define all constraints in a Zod schema and
pass it to validationSchema. This keeps validation logic out of JSX, makes schemas reusable
across the client and server, and produces consistent TypeScript types.