DEV Community

Cover image for TanStack Form vs. React Hook Form
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

TanStack Form vs. React Hook Form

Written by Amazing Enyichi Agu✏️

Building forms in React can quickly lead to a lot of code repetition and complexity if done without a good strategy. Because of this, developers manage forms with libraries like the popular React Hook Form.

But, how does TanStack Form (a newer form library) compare to React Hook Form, and should you consider using it?

This guide aims to make a fair comparison of the two libraries. At the end, I’ll give you a recommendation on which one to use based on the criteria. To follow along, you will need basic knowledge of React and TypeScript.

What is TanStack Form?

TanStack Form TanStack Form is a form management library from the team behind other popular tools like React Query (now TanStack Query), TanStack Router, and TanStack Start.

TanStack Form prioritizes type safety, flexible form validation, and render speed. The library also makes it easy to build reusable form segments and is framework-agnostic. It works with React, Vue, Angular, Solid, Lit, and Svelte.

TanStack form features

  • Guaranteed type safety: TanStack Form is written in TypeScript with strict adherence to type safety. This ensures that the library’s APIs are as type-safe as possible. The APIs also encourage type-safe development patterns. This makes forms built with TanStack Form robust and significantly reduces bugs
  • Flexible form validation: TanStack Form allows for different approaches to form validation. You can validate the form on blur, input change, submit, and even on mount. You can validate an entire form, a subsection of a form, or individual fields. TanStack Form is also compatible with validation libraries like Zod and allows a developer to write custom error messages. The library also easily enables async validation and debouncing (it has a built-in implementation)
  • Controlled form fields: TanStack Form only allows for controlled input in a form (it then manages the state of the form). According to the TanStack Form documentation, this was an intentional decision because controlled inputs have benefits like predictability and ease of testing. Working with only controlled inputs also means TanStack Form doesn’t have to interact with the DOM, so it also seamlessly works in React Native
  • Designed for large applications: TanStack Form’s APIs are developed with large applications in mind. While you can use the library for simpler form management, many of its benefits come when used in large-scale applications and forms. The library allows building reusable sections of forms (a process called form composition) and is compatible with custom design systems

How to use TanStack Form

This section shows how to use TanStack Form in a React Project. It features two tutorials: building a simple form with the library, and then a more complex form. To use TanStack Form, the documentation recommends having TypeScript v5.4 or higher installed. You also need to install the TanStack Form package. Because this tutorial will use TanStack Form for React, install the appropriate package, which is @tanstack/react-form. We’ll also use the npm package manager, but feel free to use whatever you want:

yarn npm install --save-exact @tanstack/react-form

The reason for the --save-exact flag is because of a noteworthy quirk TanStack Form has. Changes in the API types of the library are considered as patches in its semantic versioning. For this reason, it is important to lock an install to a particular patch as an upgrade could cause breaking changes in types. After installing the package, you can now use TanStack Form in React.

How to build a simple form with TanStack Form

The final source code for this tutorial can be found here on StackBlitz. To create a simple form, you need the useForm API. This API is a hook used to create a new form instance.

According to the documentation, “A form instance is an object that represents an individual form and provides methods and properties for working with the form.” This hook is used to define fields (or data options) that the form will accept. The hook is also used to define an onSubmit function that it calls whenever the form is submitted. The onSubmit function that the hook accepts will have access to the form’s responses. It is inside the onSubmit function that the developer can do whatever they want with the form responses.

This tutorial uses Tailwind CSS for styling, so here is a guide to set it up. Import the useForm Hook into your project:

import { useForm } from '@tanstack/react-form'; 
Enter fullscreen mode Exit fullscreen mode

Next, define the default values of data from the form, and then create the onSubmit function:

// ... function App() { const form = useForm({ defaultValues: { username: '', passowrd: '' }, onSubmit: ({ value }) => { // Handle the form input alert(JSON.stringify(value, null, 4)); }, }) return (/* The Component's JSX */) } export default App; 
Enter fullscreen mode Exit fullscreen mode

After that, set up the page where the form will reside:

// ... function App() { const form = useForm({ defaultValues: { username: '', passowrd: '' }, onSubmit: ({ value }) => { // Handle the form input alert(JSON.stringify(value, null, 4)); }, }) return ( <main className="mx-auto px-5 max-w-xl"> <h1 className="font-semibold my-6 text-xl">Login Form</h1> <form method="post" onSubmit={(e) => { e.preventDefault(); e.stopPropagation(); }} > {/* Add field Items here */} <button type="submit" className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-auto px-5 py-2.5 text-center cursor-pointer" onClick={() => form.handleSubmit()} > Submit </button> </form> </main> ) } //... 
Enter fullscreen mode Exit fullscreen mode

Next, add the TanStack Form Fields (note the capital F). A TanStack Form Field is a React component that comes from a form instance and manages a single form input. The component receives props like name (which must be one of the defined form options in useForm ) and validators (which contain functions for validating the field).

The Field component also accepts children, which it passes a field object to. In TanStack Form, a field object helps to control the state of an input field. Because this simple form needs two inputs (username and password), it will need two form.Field components:

// App.tsx // ... <form.Field name="username"> {(field) => ( <div className="mb-6"> <label htmlFor={field.name} className="block mb-2 text-sm font-medium text-gray-900" > Username </label> <input id={field.name} name={field.name} className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" autoComplete="off" required value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> </div> )} </form.Field> <form.Field name="passowrd" > {(field) => ( <div className="mb-6"> <label htmlFor={field.name} className="block mb-2 text-sm font-medium text-gray-900" > Password </label> <input id={field.name} name={field.name} className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" type="password" autoComplete="off" required value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} /> </div> )} </form.Field> //... 
Enter fullscreen mode Exit fullscreen mode

Now this is what the simple form looks like: Simple Form In the useForm Hook, you can also write functions for validating the form in an option called validators:

const form = useForm({ //... validators: ({value}) => {/**/} }) 
Enter fullscreen mode Exit fullscreen mode

However, for this example, the validation will be on the Field level:

<pre class="language-javascript hljs">&lt;form.Field
  name="username"
  validators={{
  onChange: ({ value }) =&gt;
    value.includes(' ') ? 'Username cannot contain space' : undefined,
  }}
&gt;
  {(field) =&gt; (
    &lt;div className="mb-6"&gt;
      &lt;label
        htmlFor={field.name}
        className="block mb-2 text-sm font-medium text-gray-900"
      &gt;
        Username
      &lt;/label&gt;
      &lt;input
        id={field.name}
        name={field.name}
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
        autoComplete="off"
        required
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.value)}
      /&gt;
      {!field.state.meta.isValid &amp;&amp; (
        &lt;em role="alert" className="text-xs text-red-600"&gt;
          {field.state.meta.errors.join(', ')}
        &lt;/em&gt;
        )}
      &lt;/div&gt;
    )}
&lt;/form.Field&gt;
&lt;form.Field
  name="passowrd"
  validators={{
    onBlur: ({ value }) =&gt;
      value.length &lt; 5 ? 'Password is too short' : undefined,
  }}
&gt;
  {(field) =&gt; (
    &lt;div className="mb-6"&gt;
      &lt;label
        htmlFor={field.name}
        className="block mb-2 text-sm font-medium text-gray-900"
      &gt;
        Password
      &lt;/label&gt;
      &lt;input
        id={field.name}
        name={field.name}
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
        type="password"
        autoComplete="off"
        required
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.value)}
      /&gt;
      {!field.state.meta.isValid &amp;&amp; (
        &lt;em role="alert" className="text-xs text-red-600"&gt;
          {field.state.meta.errors.join(', ')}
        &lt;/em&gt;
      )}
    &lt;/div&gt;
  )}
&lt;/form.Field&gt;
Enter fullscreen mode Exit fullscreen mode

Notice how flexible the form validation is. The first field validates onChange while the second validates onBlur. Custom error messages should now be visible if a user tries to submit the form: Custom Error Messages With that, you have created a simple form.

How to build a complex form with TanStack Form

While you can create simple forms with the useForm Hook, TanStack Form has much more functionality and can accommodate more complex forms. As mentioned earlier, these forms will easily fit into an existing design system. Also, TanStack Form’s APIs ultimately aim to reduce boilerplate code in the long run for large projects. The following tutorial shows how to use TanStack Form in an advanced use case.

The final source code for this section can also be found on StackBlitz here. TanStack Form integrates seamlessly with schema validation libraries like Zod or Valibot; this example uses Zod for validation. Set up a new React project. After that, install Zod:

npm i zod 
Enter fullscreen mode Exit fullscreen mode

Set up Tailwind CSS, as that is the styling framework for this example. TanStack Form recommends creating your own form hook that will have its own contexts and custom form UI components. In React, a context is a way to pass data deeply through a component tree without prop drilling.

In this case, TanStack form requires an extra step in creating contexts, then adding them to the form hook. Inside the src folder of the project, create a utilsfolder. Inside utils, create a formContext.tsfile. Inside that file, import createFormHookContexts from @tanstack/react-form. This function will be used to create the necessary form contexts:

 // utils/formContext.ts import { createFormHookContexts } from '@tanstack/react-form'; export const { formContext, fieldContext, useFieldContext } = createFormHookContexts(); 
Enter fullscreen mode Exit fullscreen mode

Next, inside the utils folder, create a formOpts.ts file. This is where you will define default values for these form fields. This file will use the formOptions helper function from @tanstack/react-form to achieve this:

//utils/formOpts.ts import { formOptions } from '@tanstack/react-form'; interface RegisterData { fullname: string; age: number; email: string; username: string; password: string; acceptTerms: boolean; } const defaultValues: RegisterData = { fullname: '', age: 0, email: '', username: '', password: '', acceptTerms: false, }; export const formOpts = formOptions({ defaultValues }); 
Enter fullscreen mode Exit fullscreen mode

Inside the utils folder, create a new file called useAppForm.ts. Inside the file, import createFormHook from @tanstack/react-form. This function will create a form hook with earlier defined contexts and custom form components. Also import the fieldContext and formContext from the src/utils/formContext.ts file:

import { createFormHook } from '@tanstack/react-form';
import { fieldContext, formContext } from './formContext';
import TextField from '../components/TextField';
import CheckField from '../components/CheckField';

const { useAppForm } = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: {
    TextField,
    CheckField,
  },
  formComponents: {},
});

export default useAppForm;
Enter fullscreen mode Exit fullscreen mode

Notice the imported TextField and CheckField added to fieldComponents. They are custom React UI components that are now bound to the useAppForm Hook.

Next, create those imported UI components. Inside the src folder, create a folder called components. The folder will contain all the reusable components in the project. Inside the components folder, create a Label.tsx file and add the following:

// components/Input.tsx
interface InputProps extends React.ComponentProps&lt;'input'&gt; {}

function Input(props: InputProps) {
  return (
    &lt;&gt;
      &lt;input
        className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
        autoComplete="off"
        {...props}
      /&gt;
    &lt;/&gt;
  );
}

export default Input;
Enter fullscreen mode Exit fullscreen mode

The above is a styled and reusable input element for the form. Next, create the TextField component referenced in utils/useAppForm.ts. This component will have access to the TanStack Form field object:

// components/TextField.tsx

import { useFieldContext } from '../utils/formContext';
import Label from './Label';
import Input from './Input';
import FieldError from './FieldError';

function TextField({ label, inputType }: { label: string; inputType: string }) {
  const field = useFieldContext&lt;string&gt;();
  return (
    &lt;&gt;
      &lt;Label htmlFor={field.name}&gt;{label}&lt;/Label&gt;
      &lt;Input
        id={field.name}
        type={inputType}
        name={field.name}
        value={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.value)}
      /&gt;
      &lt;FieldError field={field} /&gt;
    &lt;/&gt;
  );
}
export default TextField;
Enter fullscreen mode Exit fullscreen mode

The useFieldContext Hook is used to access the field object that belongs to the earlier created contexts in utils/formContext.ts. The imported FieldError component is a React component that accepts the field object and displays any error message associated with the field. Also, create a Field component for a checkbox.

This is the form field users will check to accept terms and conditions (for the tutorial form):

// components/CheckField.tsx

import { useFieldContext } from '../utils/formContext';
import FieldError from './FieldError';
import Label from './Label';

function CheckField({ label }: { label: string }) {
  const field = useFieldContext&lt;boolean&gt;();

  return (
    &lt;&gt;
      &lt;input
        type="checkbox"
        id={field.name}
        name={field.name}
        checked={field.state.value}
        onBlur={field.handleBlur}
        onChange={(e) =&gt; field.handleChange(e.target.checked)}
      /&gt;
      &lt;Label htmlFor={field.name} className="inline-block ml-2"&gt;
        {label}
      &lt;/Label&gt;
      &lt;FieldError field={field} /&gt;
    &lt;/&gt;
  );
}

export default CheckField;
Enter fullscreen mode Exit fullscreen mode

For the last reusable component, create the FieldError component:

// components/FieldError.tsx

import { type AnyFieldApi } from '@tanstack/react-form';

function FieldError({ field }: { field: AnyFieldApi }) {
  return (
    &lt;span className="block mb-5"&gt;
      {!field.state.meta.isValid &amp;&amp; (
        &lt;em className="text-red-600 text-xs"&gt;
          {field.state.meta.errors.map((err) =&gt; err.message).join(', ')}
        &lt;/em&gt;
      )}
    &lt;/span&gt;
  );
}

export default FieldError;
Enter fullscreen mode Exit fullscreen mode

Now, navigate to src/App.tsx and create the form with the useAppForm Hook. When using the useAppForm Hook, instead of form.Field component, use the form.AppField component to create the input fields:

// App.tsx

import useAppForm from './utils/useAppForm';
import { formOpts } from './utils/formOpts';

function App() {
  const form = useAppForm({
    ...formOpts,
    validators: {},
    onSubmit: ({ value }) =&gt; {
      alert(JSON.stringify(value, null, 4));
    },
  });
  return (
    &lt;main className="mx-auto px-5 max-w-xl"&gt;
      &lt;h1 className="font-semibold my-6 text-xl"&gt;Register Form&lt;/h1&gt;
      &lt;form
        method="post"
        onSubmit={(e) =&gt; {
          e.preventDefault();
          e.stopPropagation();
          form.handleSubmit();
        }}
      &gt;
        &lt;form.AppField
          name="fullname"
          children={(field) =&gt; (
            &lt;field.TextField label="Full Name" inputType="text" /&gt;
          )}
        /&gt;
        &lt;form.AppField
          name="email"
          children={(field) =&gt; (
            &lt;field.TextField label="Email" inputType="email" /&gt;
          )}
        /&gt;
        &lt;form.AppField
          name="age"
          children={(field) =&gt; (
            &lt;field.TextField label="Age" inputType="number" /&gt;
          )}
        /&gt;
        &lt;form.AppField
          name="username"
          children={(field) =&gt; (
            &lt;field.TextField label="Username" inputType="text" /&gt;
          )}
        /&gt;
        &lt;form.AppField
          name="password"
          children={(field) =&gt; (
            &lt;field.TextField label="Password" inputType="password" /&gt;
          )}
        /&gt;
        &lt;form.AppField
          name="acceptTerms"
          children={(field) =&gt; (
            &lt;field.CheckField label="I accept all terms and conditions" /&gt;
          )}
        /&gt;
        &lt;button
          type="submit"
          className="block text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-auto px-5 py-2.5 text-center cursor-pointer"
        &gt;
          Submit
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/main&gt;
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

And now you get to see the form: Complex Form

Finally, for this tutorial, validate the input using Zod and the validators property of useAppForm. Notice how the project also uses Zod to write custom error messages. The form will validate only onSubmit:

// App.tsx

import { z } from 'zod';
// Other imports

const registerSchema = z.object({
  email: z.string().email('An email is required'),
  fullname: z.string().min(3, 'Must be up to 3 letters'),
  age: z.number().min(13, 'You must be 13+ to register'),
  username: z.string().min(3, 'Must be up to 3 letters'),
  password: z.string().min(8, 'Must be up to 8 characters'),
  acceptTerms: z
    .boolean()
    .refine((value) =&gt; value, 'You must accept the terms to continue'),
});

function App() {
  const form = useAppForm({
    // ...
    validators: {
      onSubmit: registerSchema,
    }
  });
  return (/* JSX to render */);
}
export default App;
Enter fullscreen mode Exit fullscreen mode

Now the form validates on submit. Below is what a user will see when they try to submit an empty form: Complex Form With Custom Error Messages And that is how to create a complex form with TanStack Form. For even more advanced features, check out the TanStack Form documentation.

Reactivity in TanStack Form

In order to access up-to-date form values or reactive states of a form, the TanStack Form documentation recommends subscribing to the form’s states using either of two options. The first option is with the useStore Hook.

This hook is useful when you need the form states for React component logic operations. The hook accepts two arguments: the form instance’s store (which is form.store), and a function that returns the property of the form’s state to expose (or subscribe to). Here’s an example:

const fullname = useStore(form.store, (state) =&gt; state.values.fullname)
const isFormValud = useStore(form.store, (state) =&gt; state.isFormValid)
Enter fullscreen mode Exit fullscreen mode

The second method is by using the form.Subscribe component. This component has a prop called selector, which, like the second argument of useStore, accepts a function that returns what property of the form’s state to expose:

&lt;form.Subscribe selector={(state) =&gt; state.values.fullname}&gt;
    {(fullname) =&gt; (
      &lt;form.Field&gt;
        {(field) =&gt; (
          &lt;input
            name="email"
            value={field.state.email || `${fullname.replaceAll(' ', '').toLowerCase()}@neomail.com`}
            onChange={field.handleChange}
          /&gt;
        )}
      &lt;/form.Field&gt;
    )}
&lt;/form.Subscribe&gt;
Enter fullscreen mode Exit fullscreen mode

According to the documentation, form.Subscribe is best suited for when you need to react to a form state within the UI of your app. From the above example, notice how state.values.fullname is selected and passed on to the children components of form.Subscribe.

For more information on reactivity in TanStack Form, here is a more detailed explanation.

Overview of React Hook Form

React Hook Form React Hook Form (RHF) is a React form management library that emphasizes simplicity. It is the most popular form management library in React, and it also works with React Native.

Just like in TanStack Form, a developer can “subscribe” to needed form state changes in RHF, minimizing the need for re-rendering. RHF can also handle both controlled and uncontrolled form input, unlike TanStack Form. Here is a more detailed guide to the features and capabilities of React Hook Form.

TanStack Form vs. React Hook Form

The two libraries ultimately aim to solve the same problems, but they have different approaches. This section compares the libraries in categories such as developer experience (DX), features, and more.

Developer experience (DX)

TanStack Form requires more code to set up than React Hook Form, especially in large applications and projects. However, it comes with the tradeoff of reducing boilerplate code down the line. Both tools are typesafe and have IDE IntelliSense support.

Features

For basic form management, they both get the job done excellently. However, TanStack Form appears to have an edge over React Hook Form in accommodating larger-scale projects.

Bundle size

@tanstack/react-form ships with a few dependencies but still remains compact. The most consequential dependency is @tanstack/form-core (which is the main TanStack Form engine), and @tanstack/store (which is a state management library from the TanStack team used to implement the useStore functionality).

According to Bundlephobia, TanStack Form v1.11 (the latest version at the time of writing) has a size of 36.4kB when minified or 9.6kB when minified and gzipped: Bundlephobia Tanstack Form

Source: https://bundlephobia.com/package/@tanstack/[email protected]

React Hook Form, on the other hand, doesn’t ship with any dependencies. According to Bundlephobia, at v7.56 (the latest version at the time of writing), the package weighs 30.2KB when minified or 10.7 KB when minified and gzipped: Bundlephobia React Hook Form

Source: https://bundlephobia.com/package/[email protected]

Popularity

React Hook Form beats TanStack Form in popularity. Currently, React Hook Form has more stars on GitHub and more downloads on npm. TanStack Form has a smaller (but growing) community, so it might be a little harder to find support when you run into a problem.

Below is an npmtrends chart comparing both libraries over the past year: Npmtrends Comparing Both Libraries

Source: https://npmtrends.com/@tanstack/react-form-vs-react-hook-form

Maintenance

Both packages are very highly maintained. Featured below are heat maps showing contributions both packages have received over the past year. Here is one for @tanstack/react-form: Package Commits Tanstack

Source: https://pkco.vercel.app/package/@tanstack/react-form

And for react-hook-form: Package Commits React Hook Form

Source: https://pkco.vercel.app/package/react-hook-form

TL;DR comparison

TanStack Form (v1.11) React Hook Form (v7.56)
Bundle size (minified) 36.4 KB 30.2 KB
Bundle size (minified + gzipped) 9.6 KB 10.7 KB
GitHub repo stars ~ 5.4K ~ 43.1K
npm downloads (weekly) ~ 11 million ~ 219K
Supported frameworks React, Angular, Vue, Svelte, Lit, Solid React
Async form validation Yes Yes
Built-in async validation debounce Yes Not clear
Schema-based validation Yes Yes
Supports React Native Yes Yes
Supports uncontrolled form input No Yes
Supports reactive subscriptions Yes Yes

Which should you use?

It depends! If you want to build a simple form with easy-to-use APIs but less verbosity, then React Hook Form is the go-to. But for more complex forms, TanStack Form will be a better and more comprehensive solution. It’s important to note that React Hook Form is more mature than TanStack Form.

Because of this, if stability is a priority for you, go for React Hook Form (at least for now). Which works better for your project?


Get set up with LogRocket's modern React error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.