Back

TanStack Form: All-in-one Form Handling for React

TanStack Form: All-in-one Form Handling for React

Forms are very important in software applications. They are the primary means through which we interact with our users. Forms help with key user interactions, ranging from simple logins to complex data entry interfaces. Thus, ensuring efficient form handling is essential for creating seamless user experiences. This article will explore TanStack Form for form handling in React apps.

TanStack Form: All-in-one Form Handling for React Apps

TanStack Form provides a powerful solution for form handling. It is designed to tackle common form-related challenges such as:

  • Reactive data binding and state management
  • Complex validation and error handling
  • Accessibility and responsive design
  • Internationalization and localization
  • Cross-platform compatibility and custom styling

This guide will be a practical walkthrough to using TanStack Form in React apps, from basic form creation to advanced state management and validation.

Key Features

The core purpose of TanStack Form is to streamline the process of managing forms in web apps. It uses TypeScript and a headless UI component approach. It offers many helpful features. These features enable us to create dynamic and responsive forms with little effort.

Key features include:

  • TypeScript Support: Ensures type safety and robustness
  • Headless UI Components: Provides full control over form elements’ rendering and behavior
  • Framework-Agnostic Design: Integrates seamlessly with React and other frameworks

TanStack Form vs. Other Form Management Libraries

TanStack Form stands out from other form libraries in the React world. It does so for its many features, strength, and ease of use. Other libraries may focus on specific form aspects, like validation or state management. TanStack Form provides a complete solution for the whole form lifecycle.

The idea comes from the need for a clear and fast solution. It offers a complete toolkit for building forms. By using TanStack, we can speed up development and cut boilerplate code. We can also build intuitive, easy-to-use forms. This enhances our applications’ overall user experience.

Setting up TanStack Form in a React Application

Let’s integrate TanStack Form into our app with the following steps:

  1. Install TanStack dependencies We’re using npm to install the React adapter:
$ npm install @tanstack/react-form
  1. Configure it in the Project Once installed, we’d import the necessary modules at the top of the project files:
import { useForm } from "@tanstack/react-form"
import { FieldApi } from '@tanstack/react-form';
  1. Create a function for the useForm instance:
const form = useForm({
  defaultValues: {
    fullName: '',
  },
  onSubmit: async ({ value }) => {
    console.log(value)
  },
});

return();

The useForm function creates form instances. When we call the form() function, it will trigger the useForm hook to do so. The onSubmit handler automatically handles the form.

Creating Forms With TanStack

Let’s create a form instance:

<form.Provider>
  <form
    onSubmit={(e) => {
      e.preventDefault();
      e.stopPropagation();
      void form.handleSubmit();
    }}
  >
    //form.Field
    //submit button
  </form>
</form.Provider>

We must wrap the form elements in a form.Provider component. This is necessary for providing context to the form components. The code in the form element prevents the default submission of forms and stops event propagation.

Instead, it calls the handleSubmit() method of the form instance to handle the form. This method is provided by the useForm hook from the @tanstack/react-form library. It contains logic to handle the form data, such as validation, processing, and sending it to a server.

Form Field

Let’s say we want to get the name of a user:

<form.Field
  name="fullName"
  children={(field) => (
    <input
      name={field.name}
      value={field.state.value}
      onBlur={field.handleBlur}
      onChange={(e) => field.handleChange(e.target.value)}
    />
  )}
/>

We use the form.Field element to encapsulate a form input element and its associated logic. It goes inside the form element. Each input field gets its own form.Field. The name attribute specifies the name of the input field when it’s submitted as part of a form.

The name and value attributes inside the input element bind the user’s data to the field object. onBlur doesn’t do much for the form logic here. It blurs the UI when the input field is not in focus. The onChange() function calls the handleChange() method whenever the user enters the input field. This method of the field object updates the form state with the new value.

Lastly, let’s submit the form:

<button type="submit">Submit</button>

Output: tanstack-basic-form

The form field handles the user input and logs the value to the console.

Form Factory

The Form Factory is like a template for setting up forms. Let’s set one up and create an instance.

Firstly, we’ll add the import statement at the top of the file:

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

Next, we’ll create the formFactory:

const formFactory = createFormFactory({
  defaultValues: {
    firstName: "",
    lastName: "",
    age: "",
    hobbies: [],
  },
});

And then, we can create an instance just like before:

const form = formFactory.useForm({
  onSubmit: async ({ value }) => {
    console.log(value)
  },
})

Form Factory ensures consistency across multiple form instances by sharing a common configuration.

Form and Field Validation

We can choose to carry out validation at the form or field level.

Let’s add an age field with validation:

<form.Field
  name="age"
  validators={{
    onChange: ({ value }) =>
      value < 13
        ? "You must be 13 to make an account"
        : undefined,
  }}
>
  {(field) => (
    <div>
      <label htmlFor={field.name}>Age:</label>
      <input
        id={field.name}
        name={field.name}
        value={field.state.value}
        type="number"
        onChange={(e) =>
          field.handleChange(e.target.valueAsNumber)
        }
      />
      {field.state.meta.errors ? (
        <em role="alert">{field.state.meta.errors.join(", ")}</em>
      ) : null}
    </div>
  )}
</form.Field>

Here, we are performing the validation onChange(), that is, on every keystroke. We can also display errors with the field.state.meta.errors array.

Alternatively, we can set the validation after the user has finished interacting with the field:

validators={{
  onBlur: ({value}) =>
    value < 13 ? 'You must be 13 to make an account' : undefined,
}}

The error message: tanstack-validation If a user enters an age under 13, they immediately get a prompt that they need to be older.

We can run multiple event validations on the same field at the same time:

validators={{
  onChange: ({value}) =>
    value < 13 ? 'You must be 13 to make an account' : undefined,
  onBlur: ({value}) => (value < 0 ? 'Invalid value' : undefined),
}}

Similarly, we can do all our validation at the field level:

const form = useForm({
  defaultValues: {
    age: 0,
  },
  onSubmit: async ({ value }) => {
    console.log(value)
  },
  validators: {
    onChange({ value }) {
      if (value.age < 13) {
        return 'Must be 13 or older to sign'
      }
    },
  },
})

TanStack offers one of the easiest and most flexible error handling solutions so far.

Managing Form State

TanStack Form maintains the state of each form field internally. The onChange event is triggered for each stroke. The form then updates the state of the corresponding field with the new value. This state includes properties such as value, touched, errors, etc.

When the user submits the form, the onSubmit() function provided to the useForm hook is called. This function receives an object containing the form data. The form data includes the form fields and values provided by the user.

We can perform actions with the form data inside the onSubmit() function. From here, we can easily send it to a server, or update the application state.

Here, we are logging the form data to the console:

const form = formFactory.useForm({
  onSubmit: async ({ value }) => {
    console.log(value);
  },
});

tanstack-submission

The form data gets logged to the console.

Enhancing Form Functionality

There are some additional features we can use in our React forms. An example is support for asynchronous validation.

Asynchronous Form Handling

This feature allows us to perform validation logic that involves asynchronous operations. Perhaps a network request or database query.

<form.Field
  name="age"
  validators={{
    onChangeAsync: async ({ value }) => {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return value < 13 ? 'You must be 13 to make an account' : undefined;
    },
  }}
>
    
//rest of the code

The onChangeAsync() and onBlurAsync() methods define asynchronous validation rules for form fields. Just like with onChange(), onChangeAsync() is triggered on each keystroke. Similarly, onBlurAsync() gets triggered when the field loses focus. Inside these methods, we can create a promise to handle the result of the operation.

Dynamic Form Rendering

Additionally, we can dynamically generate forms by defining form fields programmatically. We can use JavaScript to dynamically create field configurations based on our requirements. For example, we can use arrays or objects to define field configurations. Then, they are passed to the form provider to generate the corresponding form fields.

Suppose we have an array of field configurations with different data types:

const fieldConfigurations = [
  { name: "firstName", label: "First Name", type: "text" },
  { name: "lastName", label: "Last Name", type: "text" },
  { name: "email", label: "Email", type: "email" },
  { name: "age", label: "Age", type: "number" },
  {
    name: "country",
    label: "Country",
    type: "select",
    options: ["Select a Country", "USA", "Canada", "UK", "Other"],
  },
  { name: "birthDate", label: "Birth Date", type: "date" },
  { name: "profilePicture", label: "Profile Picture", type: "file" },
  { name: "agreeTerms", label: "Agree to Terms", type: "checkbox" },
];

We can dynamically render it:

<form.Provider>
  {fieldConfigurations.map((field) => (
    <form.Field key={field.name} name={field.name}>
      {({ fieldApi }) => (
        <div
          key={field.name}
          style={{
            width: "440px",
            display: "flex",
            justifyContent: "space-between",
          }}
        >
          <label htmlFor={field.name}>{field.label}:</label>
          {field.type === "select" ? (
            <select {...fieldApi} id={field.name}>
              {field.options.map((option) => (
                <option key={option} value={option}>
                  {option}
                </option>
              ))}
            </select>
          ) : (
            <input {...fieldApi} type={field.type} id={field.name} />
          )}
        </div>
      )}
    </form.Field>
  ))}
  <button type="submit">Submit</button>
</form.Provider>

This code uses a simple javascript function to map through the fieldConfigurations array and create a form field for each item.

Here’s the output: tanstack-dynamic-form

A form field is created for each object in the array.

Conditional Form Rendering

There’s also the flexibility of rendering form fields conditionally based on certain criteria. Similarly, we can use JavaScript features such as conditional if statements or ternary operators. The form fields or components are rendered based on the application state or user interaction.

Here’s an example of how we can do this:

//name form field
//age form field

<form.Field
  name="hobbies"
  initialValue=""
  children={(field) => (
    <>
      <label htmlFor="option">Option:</label>
      <select
        id="option"
        name="option"
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
      >
        <option value="A">Option A</option>
        <option value="B">Option B</option>
        <option value="C">Option C</option>
      </select>
      {field.state.value === "A" && (
        <div>Content for Option A</div>
      )}
      {field.state.value === "B" && (
        <div>Content for Option B</div>
      )}
      {field.state.value === "C" && (
        <div>Content for Option C</div>
      )}
    </>
  )}
/>

This form renders additional options depending on the user’s choice. tanstack-dynamic-rendering-ezgif.com-video-to-gif-converter

The additional content rendered depends on the user’s selection.

Testing and Debugging

It is not unusual to encounter bugs while working with forms. These could be conflicts with React inbuilt state management, handling of data and so on. The console is always a good place to start when debugging. There would usually be an error message indicating the cause of the problem.

Including validations in the form prompts the user to input information in the correct format. This reduces bugs related to invalid data. It’s also a good idea to implement unit testing and integration testing in our apps. Consequently, we are able to pinpoint the cause of bugs for easy resolution.

Conclusion

This article highlights various features of TanStack Form and how we can use them in our react apps. We experimented with features like state management, dynamic rendering, validations, and others.

Through testing and debugging practices, we can identify and address potential issues. Thus ensuring that our forms maintain their reliability and functionality in production environments. Ultimately, we can streamline the form development process and deliver exceptional user experiences.

Reference

TanStack Form documentation

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay