Forms Pattern¶
Form handling with React Hook Form and Yup.
Basic Form¶
const schema = yup.object({
title: yup.string().required().max(200),
amount: yup.number().required().positive(),
date: yup.date().required(),
});
type FormData = yup.InferType<typeof schema>;
const MyForm = () => {
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: { title: "", amount: 0, date: new Date() },
});
const onSubmit = (data: FormData) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="title"
control={control}
render={({ field }) => (
<TextField
{...field}
error={!!errors.title}
helperText={errors.title?.message}
/>
)}
/>
</form>
);
};
With Mutation¶
const CreateMarkerForm = () => {
const { t } = useTranslation();
const mutation = useCreateMarker();
const { control, handleSubmit, reset } = useForm<FormData>({
resolver: yupResolver(schema),
});
const onSubmit = (data: FormData) => {
mutation.mutate(data, {
onSuccess: () => {
reset();
showSuccessToast(t(TranslationKeys.markers.created));
},
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* fields */}
<Button type="submit" loading={mutation.isPending}>
{t(TranslationKeys.shared.button.save)}
</Button>
</form>
);
};
Validation Patterns¶
const schema = yup.object({
// Required
name: yup.string().required(),
// Optional
description: yup.string().optional(),
// Conditional
endDate: yup.date().when("hasEndDate", {
is: true,
then: (schema) => schema.required(),
}),
// Custom
email: yup.string().email().test(
"not-blocked",
"This email domain is not allowed",
(value) => !BLOCKED_DOMAINS.includes(getDomain(value))
),
});
i18n in Validation¶
const schema = yup.object({
title: yup
.string()
.required(t(TranslationKeys.validation.required))
.max(200, t(TranslationKeys.validation.maxLength, { max: 200 })),
});
FormModal Component¶
Generic modal wrapper for entity create/edit forms. Handles form setup, dirty tracking, and unsaved changes.
const createSchema = (t: TranslationFunction) => yup.object({
name: yup.string().required(t(TranslationKeys.validation.required)),
});
const defaultValues = { name: "" };
const mapEntityToForm = (entity: Category) => ({
name: entity.name,
});
<FormModal<FormData, Category>
open={isOpen}
onClose={onClose}
onSubmit={handleSubmit}
createSchema={createSchema}
defaultValues={defaultValues}
entity={editingEntity}
mapEntityToForm={mapEntityToForm}
>
{({ control, errors, isEditMode }) => (
<>
<Controller name="name" control={control} render={...} />
<FormErrorAnnouncer errors={errors} />
</>
)}
</FormModal>
Props: - createSchema — Function receiving t translator, returns Yup schema - defaultValues — Default form values (must be stable reference via useMemo or defined outside component) - entity — Entity for edit mode (null for create mode) - mapEntityToForm — Maps entity to form values for edit mode
Features: - Automatic form reset when modal opens - Dirty state tracking - Unsaved changes confirmation on close - Edit mode detection via isEditMode render prop
Related¶
- API Layer — Mutations
- State Management — Stores