Forms & Validation
We build forms with react-hook-form and validate with zod, wired
together by @hookform/resolvers. The schema is the single source of truth — the
form’s TypeScript types are inferred from it, so validation and types can never
drift apart.
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Text, TextInput } from "react-native";
const schema = z.object({
email: z.string().email("Enter a valid email"),
amount: z.coerce.number().positive("Amount must be greater than 0"),
});
type FormValues = z.infer<typeof schema>;
export function PaymentForm({ onValid }: { onValid: (v: FormValues) => void }) {
const {
control,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({ resolver: zodResolver(schema) });
return (
<>
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<TextInput value={value} onChangeText={onChange} autoCapitalize="none" />
)}
/>
{errors.email && <Text>{errors.email.message}</Text>}
{/* submit */}
{/* onPress={handleSubmit(onValid)} */}
</>
);
}
Key points:
- Wrap each native input in a
Controller(React Native inputs aren’t uncontrolled DOM nodes, so RHF needs the adapter). - Read errors from
formState.errors.<field>?.message. z.coerce.number()turns the string aTextInputproduces into a number before validating.
Reuse zod schemas beyond forms — validate API responses with the same schema
(schema.parse(response)) so bad data fails loudly at the boundary instead of
deep inside a screen.