Los componentes TextInput de React Native siempre se controlan desde el lado nativo, pero aún así quieres un estado de formulario no controlado en el hilo JS. react-hook-form se suscribe a cambios de campo en lugar de re-renderizar la pantalla completa en cada pulsación de tecla - el patrón que mantiene suave en dispositivos Android de gama media los flujos de registro de múltiples campos y pago.
useForm contiene valores de campo en una tienda respaldada por ref - actualizar un campo no re-renderiza el padre a menos que te suscribas a ese segmento de formState.
Controller registra el campo, conecta onChange a onChangeText, y adjunta el ref para que setFocus pueda llamar a TextInput.focus().
La validación se ejecuta a través de rules en Controller, un resolver personalizado, o zodResolver (ver Validación con Zod).
handleSubmit ejecuta validación primero; tu onSubmit solo se dispara cuando el formulario es válido.
formState usa un Proxy - leer errors.email suscribe el componente a errores de correo; leer todo el objeto formState se suscribe a todo.
import { useForm, type SubmitHandler } from "react-hook-form";import { z } from "zod";import { zodResolver } from "@hookform/resolvers/zod";const ProfileSchema = z.object({ fullName: z.string().min(1), email: z.string().email(),});type ProfileValues = z.infer<typeof ProfileSchema>;const onSubmit: SubmitHandler<ProfileValues> = async (values) => { // los valores están completamente tipados};const form = useForm<ProfileValues>({ resolver: zodResolver(ProfileSchema), defaultValues: { fullName: "", email: "" },});
Instala con npx expo install react-hook-form @hookform/resolvers zod.
Usa FieldPath<ProfileValues> en props name de campo reutilizable para que los typos fallen en tiempo de compilación.
Los valores de TextInput siempre son strings - coerciona números en Zod con z.coerce.number() en lugar de escribir campos como number en defaultValues.
Propagación de register en TextInput - register devuelve props centrados en web (onChange, name) que no se asignan a onChangeText. Solución: Usa Controller y asigna onChange → onChangeText.
Falta de claves defaultValues - Los nuevos campos muestran undefined, isDirty permanece false después de ediciones, y reset() los salta. Solución: Lista cada campo en defaultValues, incluyendo strings vacíos y arrays vacíos.
Desestructuración de todo formState - const { formState } = useForm() luego leer formState.isSubmitting en render aún funciona, pero const { isSubmitting, errors, isDirty, isValid } = formState se suscribe a cuatro señales a la vez. Solución: Desestructura solo lo que el componente muestra.
watch() en el render padre - watch() sin argumentos re-renderiza en cada keystroke en todos los campos - el problema que RHF resuelve. Solución:watch("email") para un campo, o mueve UI vigilada a un hijo aislado.
Valor undefined de TextInput - Pasar value={undefined} dispara advertencias y limpia la posición del cursor. Solución:value={value ?? ""} en el render Controller.
onSubmit sin handleSubmit - Presionar el botón salta validación y envía datos parciales. Solución:onPress={handleSubmit(onSubmit)} en Pressable, nunca onPress={onSubmit}.
Errores del servidor no asignados a campos - API devuelve { fieldErrors: { email: "Taken" } } pero la interfaz de usuario muestra una alerta genérica. Solución: Itera setError("email", { type: "server", message }) y setFocus("email").
¿Cómo muestro un estado de carga en el botón de envío?
Desestructura isSubmitting de formState.
handleSubmit lo establece en true mientras tu promesa onSubmit async está pendiente.
Deshabilita Pressable y cambia el texto de la etiqueta mientras isSubmitting sea true.
¿Puedo usar react-hook-form dentro de un FlatList?
Sí para el envoltorio del formulario - coloca campos dentro de ListHeaderComponent o ListFooterComponent.
No coloques TextInput no controlado dentro de renderItem sin Controller - el reciclaje restablecerá valores.
Para filas dinámicas, prefiere useFieldArray sobre estado de índice manual.
¿Cómo reinicio el formulario después de una llamada API exitosa?
async function onSubmit(values: RegisterValues) { await createAccount(values); reset(); // de vuelta a defaultValues}
reset() limpia errores, estado sucio y valores.
Pasa reset(partialValues) para mantener algunos campos después del éxito (p. ej., correo para un paso de confirmación).
¿Debo usar FormProvider?
Úsalo cuando múltiples componentes secundarios necesiten useFormContext() - pasos de asistente, grupos de campos modal, o secciones de formulario de sistema de diseño.
Para un formulario de una pantalla, pasar control a TextField es suficiente.
¿Cómo se compara react-hook-form con useState controlado en RN?
useState re-renderiza el componente propietario en cada onChangeText.
RHF almacena valores en refs y solo re-renderiza segmentos formState suscritos.
Para 1-2 campos la diferencia es negligible; pasado ~4 campos RHF gana en hardware de gama media.
¿Cómo manejo texto seguro y alternadores de visibilidad de contraseña?
Mantén secureTextEntry en TextInput dentro de Controller - no bifurques un segundo input no controlado.
Alterna visibilidad con useState local en el componente de campo; el valor RHF permanece como la misma cadena.
¿Puedo combinar react-hook-form con KeyboardAwareScrollView?
Sí - envuelve el formulario en KeyboardAwareScrollView desde react-native-keyboard-controller.
Establece keyboardShouldPersistTaps="handled" para que los botones de envío funcionen mientras el teclado está abierto.
Ver Keyboard Controller para bottomOffset y configuración de barra de herramientas.
¿Cómo instalo react-hook-form en un proyecto Expo SDK 57?
npx expo install react-hook-form
Añade @hookform/resolvers y zod cuando conectes validación de esquema.
No se requiere plugin de configuración nativa - JavaScript puro.