Jagadhiswaran Devaraj

Jun 10, 2025 • 4 min read

How React Hook Form Works Under the Hood - With Zod, tRPC, and Resolvers

Understanding the Engine Behind React Hook Form and Its Schema-Agnostic Validation Layer

How React Hook Form Works Under the Hood - With Zod, tRPC, and Resolvers

React Hook Form (RHF) has become the go-to form library for React developers. Its minimal re-render strategy, flexible API, and broad ecosystem support (Zod, Yup, Joi, tRPC, and more) make it incredibly powerful. But have you ever wondered how it works internally? What are resolvers? How is it able to connect seamlessly with schema validators and even backend-aware tools like tRPC?

In this article, we’ll unpack:

  • How RHF manages form state and performance

  • The architecture behind useForm and useController

  • What resolvers are and how they abstract validation

  • How RHF plugs into Zod, Yup, Joi, and tRPC

  • How to create a custom resolver

  • Deep dive into resolver lifecycle and optimization

Let’s dive deep.


1. Core Architecture of React Hook Form

At the heart of RHF is its uncontrolled component philosophy. Instead of syncing form state on every keystroke (as controlled components do), RHF relies on the DOM as the source of truth similar to how native forms behave.

useForm creates the Form Context

When you call useForm(), RHF creates a form state machine internally:

  • Fields registry: A map of input refs and metadata

  • Form state: Touched, dirty, errors, values

  • Validation strategy: Sync, async, resolver-based

  • Watchers: Subscriptions for value changes

const { register, handleSubmit, formState } = useForm();

Registration

Each register call binds the input's ref and event listeners (onChangeonBlur) to RHF's internal state machine. Instead of using React state for every input, RHF listens directly to DOM events for better performance.

<input {...register("email")} />

2. Controlled vs Uncontrolled Inputs

RHF prefers uncontrolled inputs because:

  • Less re-renders (no setState)

  • Smaller memory footprint

  • Easier integration with native form behavior

But it also supports controlled inputs via useController() or Controller component:

const { control } = useForm();
const { field } = useController({ name: "email", control });
<input {...field} />

This wraps controlled components (like React Select) while still syncing with RHF's internal form state.


3. How Validation Works in RHF

RHF supports two types of validation:

  • Built-in rules (e.g., requiredminLengthpattern)

  • Resolvers (external schema validation)

For built-in rules:

<input {...register("email", { required: true })} />

This uses a fast, imperative check on blur/change events.

For schema-based validation, RHF uses resolvers.


4. Resolvers: Schema-Based Validation Interface

resolver is a function that bridges RHF and a schema library (like Zod/Yup).

Resolver signature

type Resolver = (
  values: any,
  context: any,
  options: {
    criteriaMode?: 'firstError' | 'all',
    fields: Field[],
  }
) => Promise<{ values: any, errors: FieldErrors }>

Resolvers provide an abstraction layer that decouples form state management from validation logic. RHF uses the resolver as a plug-and-play contract for schema libraries.

When a form is submitted or validation is triggered, RHF calls the resolver with:

  • The current values

  • Optional validation context

  • Field-level info (to scope validation)

What the Resolver Does Internally

  1. Parses/validates the input data using the schema

  2. Normalizes the result:

    • If valid: returns { values, errors: {} }

    • If invalid: returns { values: {}, errors }

  3. RHF consumes the normalized result and updates the formState.errors

Resolvers are executed only when needed, based on mode (e.g., onChangeonBluronSubmit). RHF avoids unnecessary re-validations by comparing previous values and state snapshots.


5. Using Zod with RHF

import { zodResolver } from '@hookform/resolvers/zod';

const schema = z.object({
  email: z.string().email(),
});

const { register, handleSubmit } = useForm({
  resolver: zodResolver(schema),
});

The zodResolver wraps Zod’s .parse logic, catching errors and transforming them into RHF’s FieldError shape:

{
  email: {
    type: "zod",
    message: "Invalid email address"
  }
}

This keeps validation consistent and allows for nested and deeply structured schemas.


6. How RHF Supports Multiple Libraries

Each library (Yup, Zod, Joi, Superstruct) defines its own schema and error shape. RHF doesn’t depend on any of themdirectly.

Instead, it offloads validation to a resolver:

  • The resolver adapts the library’s validation result to RHF’s format

  • You can swap Zod for Yup or Joi by just changing the resolver

This decoupling is key to RHF’s flexibility.

Resolver Examples:

  • zodResolver(schema)

  • yupResolver(schema)

  • joiResolver(schema)

  • superstructResolver(schema)

You can even build your own custom resolver.


7. Using RHF with tRPC

tRPC provides end-to-end type-safe APIs. You can validate inputs using Zod schemas both on the frontend and backend.

Client-side validation with RHF + tRPC

Use the same schema as in the tRPC router:

const { register, handleSubmit } = useForm({
  resolver: zodResolver(myTRPCInputSchema),
});

Then on submit, pass data to the tRPC mutation:

const mutation = trpc.user.create.useMutation();

const onSubmit = (data) => {
  mutation.mutate(data);
};

This ensures your data is validated before it ever hits the backend.


8. Creating a Custom Resolver

If you're using a custom validation library or want special logic, you can build your own resolver:

const customResolver: Resolver = async (values) => {
  const errors = {};

  if (!values.email?.includes("@")) {
    errors.email = {
      type: "manual",
      message: "Invalid email",
    };
  }

  return {
    values: Object.keys(errors).length ? {} : values,
    errors,
  };
};

const form = useForm({ resolver: customResolver });

This pattern allows you to:

  • Enforce domain-specific rules

  • Integrate with external APIs

  • Handle cross-field validations

You can also leverage caching or memoization within your resolver to avoid expensive validations on every keystroke.


Final thoughts

React Hook Form’s power comes from:

  • Leveraging uncontrolled inputs to avoid excessive re-renders

  • A form state machine that tracks errors, touched, dirty, and values

  • Resolvers that abstract validation logic and support any schema lib

  • A plugin-like architecture that allows easy integration with Zod, Yup, tRPC, and more

Understanding how RHF works behind the scenes helps you:

  • Debug form issues more easily

  • Write better custom validations

  • Choose the right integration approach for your stack

React Hook Form isn’t just fast it’s deeply extensible.

- Jagadhiswaran Devaraj


📢 Stay Connected & Dive Deep into Tech!

🚀 Follow me for hardcore technical insights on JavaScript, Full-Stack Development, AI, and Scaling Systems:

Let’s geek out over code, architecture, and all things in tech!

Join Jagadhiswaran on Peerlist!

Join amazing folks like Jagadhiswaran and thousands of other people in tech.

Create Profile

Join with Jagadhiswaran’s personal invite link.

0

17

0