Going Pro: Job-Ready React NativeExtra· 40 min read

Forms at Scale with React Hook Form

One TextInput wired to state is fine — a sign-up form with five fields and validation needs React Hook Form to stay clean.

What you will learn

  • See why many controlled inputs get unwieldy
  • Build a form with React Hook Form
  • Validate fields and show clear error messages

When useState forms stop scaling

Earlier you built a form by giving each TextInput its own useState. That is fine for one or two fields. But a real sign-up form has name, email, password, confirm password, maybe a phone number — each needing its own state, its own onChangeText, and its own validation (is the email valid? is the password long enough?). Doing all that by hand gets long and error-prone fast.

Validation means checking the user typed something acceptable before you accept it — a real email, a password of at least 8 characters. React Hook Form is a popular library that manages all the fields, their values and their validation for you, with very little code. It is taught in most modern courses for exactly this.

Install it (one-time):

Install React Hook Form
npx expo install react-hook-form

Note: Output: (Adds the package. Nothing on screen yet.)

A login form with validation

React Hook Form gives you a useForm hook. From it you take control (which connects inputs to the form), handleSubmit (which runs your code only if everything is valid), and formState.errors (the current error messages). You connect each TextInput with a Controller. Here is a login form that requires a valid email and a password of at least 6 characters:

React Hook Form manages values, validation and error messages
import { useForm, Controller } from 'react-hook-form';
import { TextInput, Text, Button, View } from 'react-native';

export default function LoginForm() {
  const { control, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => console.log('Valid!', data);

  return (
    <View>
      <Controller
        control={control}
        name="email"
        rules={{ required: 'Email is required', pattern: { value: /@/, message: 'Enter a valid email' } }}
        render={({ field: { value, onChange } }) => (
          <TextInput placeholder="Email" value={value} onChangeText={onChange} />
        )}
      />
      {errors.email && <Text style={{ color: 'red' }}>{errors.email.message}</Text>}

      <Controller
        control={control}
        name="password"
        rules={{ required: 'Password is required', minLength: { value: 6, message: 'At least 6 characters' } }}
        render={({ field: { value, onChange } }) => (
          <TextInput placeholder="Password" secureTextEntry value={value} onChangeText={onChange} />
        )}
      />
      {errors.password && <Text style={{ color: 'red' }}>{errors.password.message}</Text>}

      <Button title="Log in" onPress={handleSubmit(onSubmit)} />
    </View>
  );
}

Note: Output: Two boxes and a "Log in" button. Tap Log in with an empty email -> "Email is required" appears in red. Type "abc" (no @) -> "Enter a valid email". Give a valid email and a 6+ character password -> the console logs: Valid! { email: ..., password: ... } handleSubmit(onSubmit) only calls onSubmit when every rule passes, so your real code never runs on bad data.

How the form runs, step by step

Following one tap of "Log in", in order:

  1. useForm() creates the form and hands you control, handleSubmit and errors.
  2. Each Controller registers one field by name ("email", "password") and connects a TextInput to the form via value and onChange — you no longer write a useState per field.
  3. The rules on each Controller describe what counts as valid: required, a pattern for the email, a minLength for the password.
  4. When the user taps the button, handleSubmit checks every field against its rules.
  5. If anything fails, handleSubmit fills formState.errors and your onSubmit does not run; the errors.email.message lines render the red messages.
  6. If everything passes, handleSubmit calls onSubmit(data) with a tidy object of all the values, ready to send to your login API.

Tip: For complex rules, pair React Hook Form with a schema library like Zod or Yup: you describe the whole form’s rules in one object and the form validates against it. Great for multi-step sign-ups where the same rules are reused.

Watch out: Show errors next to the field, only after the user has tried to submit or left the field — flashing red on an empty form the moment it loads feels hostile. formState.errors populating on submit (as above) gives that friendlier timing for free.

Q. What does handleSubmit(onSubmit) do when a rule fails?

Answer: handleSubmit validates first: on failure it populates formState.errors (which you render) and never calls your onSubmit, so your real logic only runs on valid data.

✍️ Practice

  1. Add a "confirm password" field and a rule that it must match the password.
  2. Add a required "name" field with a minLength of 2 and show its error message.

🏠 Homework

  1. Build a sign-up form (name, email, password) with React Hook Form, validate each field, and log the values only when all are valid.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →