Skip to content

Taming Forms With react-hook-form

Taming Forms With react-hook-form

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Taming Forms With react-hook-form

After some time of doing repetitive tasks like handling forms in React.js, you will notice that there is a lot of boilerplate code that can be abstracted into reusable custom hooks. Luckily, there is plenty of existing Open Source solutions. In this case, we will be using react-hook-form.

What is react-hook-form

react-hook-form is a performant, flexible, and extensible form handling library built for React. It exports a custom hook that you can call within your Functional Components, and returns both a register function that you pass as a ref to your input components, and a handleSubmit function to wrap your submit callback.

By returning a register function that will be added to the input component, we can leverage the Uncontrolled Component pattern to make our application faster and more performant, by avoiding unnecessary re-renders.

What are we going to build?

To get a better understanding of what react-hook-form can do, we will build a simple application showing a list of characters and a form to add them to our list.

Screen-Recording-2020-10-19-at-1

Application Setup

Before going right into react-hook-form, we will need to prepare our application with the basic file structure and functionality. For this, we will create a new react application (you can either use your preferred starter or cloud IDE).

If you want to skip the application setup, you can go ahead and fork this CodeSandbox, but I highly recommend you at least read this section to have a better understanding of what the app does.

1. Characters List

Let's start by creating a new component where we will display our characters.

character-list.js

import React from "react";

function CharacterList({ characters }) {
  return (
    <div>
      <h2>Character List</h2>

      {characters.length === 0 ? (
        <p>
          <em>Your character list is empty</em>
        </p>
      ) : (
        <ul>
          {characters.map((character, id) => (
            <li key={id}>
              {character.name} (<strong>{character.species}</strong>)
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default CharacterList;

If you have a basic understanding of React, you will notice our CharacterList component will receive a characters prop, which is an array of objects with the properties name and species. If the array is empty, we will render a placeholder. Elsewhere, we will render the list.

2. Add Character Form

The second step is to create a component that will render the form to add a new character to our list.

character-form.js

import React from "react";

function CharacterForm({ addCharacter }) {
  const onSubmit = (data) => {
    addCharacter(data);
  };

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
          <input name="name" placeholder="Character name" />
        </div>
        <div>
          <select name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

By itself, this component won't do anything because we are not doing anything with the collected data, nor validating our fields. This will be the component where we will be working on the next section of this tutorial.

3. The App

Now, let's just create the App component where we will render CharacterList and CharacterForm.

app.js

import React from "react";

import CharacterList from "./character-list";
import CharacterForm from "./character-form";

function App() {
  const [characters, setCharacters] = React.useState([]);

  const addCharacter = (character) => {
    setCharacters((characters) => [...characters, character]);
  };

  return (
    <div>
      <CharacterList characters={characters} />
      <hr />
      <CharacterForm addCharacter={addCharacter} />
    </div>
  );
}

export default App;

We will be saving our character list in characters by using the React.useState hook, and passing them down to CharacterList. Also, we created an addCharacter function that will just add a new character at the end of the characters list, and pass it to CharacterForm via prop.

Let's get to it!

Now that we have our application setup, let's see how can we leverage react-hook-form to take our forms to the next level.

Install react-hook-form

yarn add react-hook-form

Add react-hook-form to your CharacterForm

Here comes the fun. First, let's import useForm from react-hook-form, call the hook in our component, destructure register and handleSubmit methods out of it (don't worry, I will explain what they do just in a while), wrap our onSubmit function with handleSubmit, and pass register as the ref for each one of our form controls.

character-form.js

import React from "react";
+import { useForm } from "react-hook-form";

function CharacterForm({ addCharacter }) {
+  const { register, handleSubmit } = useForm();
+
-  const onSubmit = (data) => {
-    addCharacter(data);
-  };
+  const onSubmit = handleSubmit((data) => {
+    addCharacter(data);
+  });

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
-          <input name="name" placeholder="Character name" />
+          <input ref={register} name="name" placeholder="Character name" />
        </div>
        <div>
-          <select name="species">
+          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

The register method

By attaching the register ref to our form controls, we can start tracking some stuff like the field value, its validation status, and even if the field had been touched or not.

Important: the name prop is required when passing the register ref, and it should be unique. This way, react-hook-form will know where to assign the field value. For more information, check out the register documentation.

The handleSubmit method

This is a function that wraps our submit callback, and passes the actual form values to it. Under the hood, it also calls preventDefault on the form event to avoid full page reloads. It can also be an asynchronous function.

For more information, check out the handleSubmit documentation.

Add some validations

At this point, we have a working form that is able to add characters to our list. However, we are not checking if the field is filled, in order to avoid empty submissions.

With react-hook-form, it is as simple as calling the register function with a configuration object defining the validation rules. For our case, we will make the name field required. Also, we can extract errors from useForm to show the user if the field has errors.

import React from "react";
import { useForm } from "react-hook-form";

function CharacterForm({ addCharacter }) {
-  const { register, handleSubmit } = useForm();
+  const { register, handleSubmit, errors } = useForm();

  const onSubmit = handleSubmit((data) => {
    addCharacter(data);
  });

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
-          <input ref={register} name="name" placeholder="Character name" />
+          <input
+            ref={register({ required: true })}
+            name="name"
+            placeholder="Character name"
+          />
+          {errors.name && errors.name.type === "required"
+            ? "Name is required"
+            : null}
        </div>
        <div>
          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

Reset the form status

The final step is to clear our form after successfully adding a character to our character list. For that, we will destructure a new method from the useForm hook: reset, and call it after addCharacter.

import React from "react";
import { useForm } from "react-hook-form";

function CharacterForm({ addCharacter }) {
-  const { register, handleSubmit, errors } = useForm();
+  const { register, handleSubmit, errors, reset } = useForm();

  const onSubmit = handleSubmit((data) => {
    addCharacter(data);
+    reset();
  });

  console.log(errors.nameRequired);

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
          <input
            ref={register({ required: true })}
            name="name"
            placeholder="Character name"
          />
          {errors.name && errors.name.type === "required"
            ? "Name is required"
            : null}
        </div>
        <div>
          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

For more information, check out the reset documentation.

Moving forward

Now that you have a better sense of how to manage your React forms, you have unlocked a new world of possibilities by using battle-tested and community-validated libraries like react-hook-form.

You can take a look at more advanced use cases, additional resources or even take a look at the full API.

If you want a finished code sample, you can check out this CodeSandbox.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

Communication Between Client Components in Next.js cover image

Communication Between Client Components in Next.js

Communication Between Client Components in Next.js In recent years, Next.js has become one of the most popular React frameworks for building server-rendered applications. With the introduction of the App Router in Next.js 13, the framework has taken a major leap forward by embracing a new approach to building web applications: the concept of server components and client components. This separation of concerns allows developers to strategically decide which parts of their application should be rendered on the server and which are then hydrated in the browser for interactivity. The Challenge: Communicating Between Client Components While server components offer numerous benefits, they also introduce a new challenge: how can client components within different boundaries communicate with each other? For instance, let's consider a scenario where you have a button (a client component) and a separate client component that displays the number of times the button has been clicked. In a regular React application, this would typically be accomplished by lifting the state to a common ancestor component, allowing the button to update the state, which is then passed down to the counter display component. However, the traditional approach may not be as straightforward in a Next.js application that heavily relies on server components, with client components scattered across the page. This blog post will explore three ways to facilitate communication between client components in Next.js, depending on where you want to store the state. Lifting State to a Common Client Component One approach to communicating between client components is to lift the state to a common client component. This could be a React context provider, a state management system like Zustand, or any other solution that allows you to share state across components. The key aspect is that this wrapper component should be higher up in the tree (perhaps even in the layout) and accept server components as children. Next.js allows you to interleave client and server components as much as you want, as long as server components are passed to client components as props or as component children. Here's how this approach might look in practice. First of all, we'd create a wrapper client component that holds the state: ` This wrapper component can be included in the layout: ` Any server components can be rendered within the layout, potentially nesting them several levels deep. Finally, we'd create two client components, one for the button and one for the counter display: ` ` The entire client and server component tree is rendered on the server, and the client components are then hydrated in the browser and initialized. From then on, the communication between the client components works just like in any regular React application. Check out the page using this pattern in the embedded Stackblitz window below: Using Query Params for State Management Another approach is to use query params instead of a wrapper client component and store the state in the URL. In this scenario, you have two client components: the button and the counter display. The counter value (the state) is stored in a query param, such as counterValue. The client components can read the current counter value using the useSearchParams hook. Once read, the useRouter hook can update the query param, effectively updating the counter value. However, there's one gotcha to this approach. If a route is statically rendered, calling useSearchParams will cause the client component tree up to the closest Suspense boundary to be client-side rendered. Next.js recommends wrapping the client component that uses useSearchParams in a boundary. Here's an example of how this approach might look. The button reads the current counter value and updates it on click by using the router's replace function: ` The counter display component is relatively simple, only reading the counter value: ` And here is the page that is a server component, hosting both of the above client components: ` Feel free to check out the above page in the embedded Stackblitz below: Storing State on the Server The third approach is to store the state on the server. In this case, the counter display component accepts the counter value as a prop, where the counter value is passed by a parent server component that reads the counter value from the database. The button component, when clicked, calls a server action that updates the counter value and calls revalidatePath() so that the counter value is refreshed, and consequently, the counter display component is re-rendered. It's worth noting that in this approach, unless you need some interactivity in the counter display component, it doesn't need to be a client component โ€“ it can be purely server-rendered. However, if both components need to be client components, here's an example of how this approach might look. First, we'll implement a server action that updates the counter value. We won't get into the mechanics of updating it, which in a real app would require a call to the database or an external API - so we're only commenting that part. After that, we revalidate the path so that the Next.js caches are purged, and the counter value is retrieved again in server components that read it. ` The button is a simple client component that calls the above server action when clicked. ` The counter display component reads the counter value from the parent: ` While the parent is a server component that reads the counter value from the database or an external API. ` This pattern can be seen in the embedded Stackblitz below: Conclusion Next.js is a powerful framework that offers various choices for implementing communication patterns between client components. Whether you lift state to a common client component, use query params for state management, or store the state on the server, Next.js provides all the tools you need to add a communication path between two separate client component boundaries. We hope this blog post has been useful in demonstrating how to facilitate communication between client components in Next.js. Check out other Next.js blog posts we've written for more insights and best practices. You can also view the entire codebase for the above snippets on StackBlitz....

Improving INP in React and Next.js cover image

Improving INP in React and Next.js

Improving INP in React and Next.js In one of my previous articles, I've explained what INP is, how it works, and how it may affect your website. I also promised you to follow up with more concrete advice on how to improve your INP in your favorite framework. This is the follow-up article, where I'll focus on how to improve your INP score in React and Next.js. How to prepare for INP in React and Next.js? The first thing to do is to ensure you're using the latest version of React. The React team has been working on making React more INP-friendly and has already made some improvements in the latest versions. To enhance your INP score, consider fully taking advantage of new features introduced in React 18, such as Concurrent Rendering, Automatic Batching, and Selective Hydration. However, there are also some general areas to focus on, such as SSR and SSG in Next.js, Web Workers, or optimizing your hooks and state management. Concurrent Rendering The Concurrent Mode in React uses an algorithm that breaks rendering down into so-called "fiber nodes" and schedules the renders based on their expiration and priority. This effectively allows the user to interact with the page while the rendering is still in progress. In previous React versions, all updates, such as setState calls were treated as "urgent" and once the re-render had started, there was no way to interrupt it. Concurrent Mode changes this by being able to prioritize the updates and interrupt a non-blocking state update started with startTransition. For a simple explanation of concurrency in React, you can check out Dan Abramov's explanation. As part of the Concurrent Mode, React introduced several lifecycle methods that allow you to prioritize the rendering of certain parts of your UI, such as: - useTransition hook that allows you to update the state without blocking the UI, - useDeferredValue hook that allows you to defer the rendering of certain parts of your UI, - startTransition API that, similarly to the useTransition hook lets you mark a state update as non-blocking. It lacks, however, an indication of whether it's still pending. Automatic Batching Introduced in React 18, Automatic Batching reduces the number of re-renders that happen on state changes even when they happen outside of event handlers, e.g. in a setTimeout or Promise callback. This feature comes out of the box and you don't have to do anything to enable it, and it makes a great argument for upgrading to React 18. Selective Hydration Selective Hydration allows you to take hydration off the main thread by wrapping your components in a Suspense boundary. This way, components can become interactive faster as the browser can do other work on the main thread while the hydration is happening. To fully take advantage of selective hydration, consider the following: - Prioritizing Above-the-Fold Content: Use Suspense boundaries strategically around any parts of your application that may take the server longer to deliver to ensure they donโ€™t block critical content from becoming interactive as soon as possible. - Hydration on Interaction: Implementing hydration upon user interaction for non-critical components can drastically reduce the main thread's workload, enhancing INP. Vercel even has a small case study showing how using selective hydration improved the performance of a Next.js site. Server-Side Rendering (SSR) and Static Site Generation (SSG) in Next.js Not everything has to run client-side. Next.js excels in SSR and SSG capabilities, which can significantly impact INP by delivering content to users faster. Optimizing SSR with techniques like incremental static regeneration (ISR) or leveraging SSG for static pages ensures that users can interact with content faster, improving the perceived performance. Workers Offloading heavy computations to Web Workers can free up the main thread, enhancing the responsiveness of React and Next.js applications. This strategy is especially useful when dealing with third-party scripts. Offloading such scripts in Next.js can be easily done by specifying the "worker" strategy on your Script component. Be aware that this feature is not yet stable and does not work with the app directory, though. If you want to take things one step further, you could use Partytown, which helps you offload any resource-intensive scripts to Web Workers. It comes with a React component that you can use to wrap your third-party scripts and offload them to a Web Worker, and it's compatible with Next.js as well. Hooks and State Management State management in React applications can easily get out of hand, leading to unnecessary re-renders and effectively an increased INP. Sometimes, using a state management library like Redux or MobX can help you consolidate your state and reduce the number of re-renders. However, they are not silver bullets and can also introduce performance issues if not used properly. If you are dealing with a lot of re-renders due to prop changes, make sure you are leveraging memoization. As of now, you may need to work with useMemo and useCallback hooks to memoize your values and functions, respectively. The upcoming React 19โ€™s Forget Compiler, however, will apparently memoize everything under the hood, making these hooks obsolete. Using memoization properly can help you reduce the number of re-renders and improve your INP. To investigate your hook dependencies and re-renders, you can leverage React Developer Tools or use this handy helper hook I found on the internet to trace your re-renders: ` Conclusion Improving INP in React and Next.js is not easy and can require much investigation and fine-tuning. Still, it's worth doing to avoid being penalized by Google in its search results and provide a better experience for your users. Adopting React 18's new features, leveraging SSR and SSG in Next.js, utilizing Web Workers, and optimizing hooks and state management can significantly boost your INP score and deliver a faster application to your users. Remember, INP is just one among many performance metrics emphasizing the need for a comprehensive approach to performance optimization...

What's New in Next.js cover image

What's New in Next.js

What's new in Next.js On October 27th 2020, the first ever Next.js Conf streamed to an audience of more than 34K. Let's go over the most important highlights! Next.js v10 The new version of Vercel's framework was released, boasting exciting new features that directly affect website performance and Web Vitals Metrics, allowing developers to have a better impact in search results. So what are these features? New Image Component One of the core metrics of the Web Vitals is the Largest Contentful Paint (LCP), and it directly affects user experience as a web application loads. Often, images are not properly optimized to have different behaviour based on the visitor device. For example, you may be downloading a 1000x1000 pixels image and only rendering a 200x200 pixels image on mobile devices. Luckily, this new version introduces a new Image component that acts as a drop-in replacement for the HTML element, with the advantage that comes with built-in optimization for different devices. Under the hood, images rendered with the new Image compoonent will be lazy-loaded, and will only render when they are within the viewport. Migration Integrating the new Image component is as easy as replacing all img elements with the new component: *Before* ` *After* ` One important thing to note is that the width and height props are required. Although one may think it will affect responsive layouts, this is not the case, because the image will be automatically made responsive based on the aspect ratio from the provided dimensions. Internationalization After 2 months of gathering feedback from the community, in an RFC, the ability to add i18n to a Next.js application is now built into the framework. Before this release, it was only possible through a custom server, which is not encouraged anymore by the Vercel team. Instead, you can now easily configure a Next.js application to have i18n capabilities by just adding some extra parameters to the configuration file: next.config.js ` The routing can happen at two different levels; subpath routing (adds the locale as an URL path, i.e. https://miwebsite.com/es/about) and domain routing (reads the locale from an absolute URL, i.e https://mywebsite.es). To enable domain routing, the configuration file will need to be slightly different: next.config.js ` This new version also has automatic language detection on the / route by checking the Accept-Language header, which will be matched against the provided configuration. Improved Link component Trying to create a link to a dynamic route by using a combination of the href and as props was somewhat confusing before. However, now the next/link component only needs the href prop. *Before:* ` *After:* ` This is not a breaking change so you can still continue using both props. However, now it is more developer-friendly. Blocking fallback for getStaticPaths What makes Next.js stands over other frameworks is the ability to incrementally generate static pages. This means, if you are building an e-commerce platform with *lots* of products, you won't need to statically generate all pages at build time. Instead, you can opt in to incrementally generate the pages in getStaticPaths. Starting in Next.js 9.3, you could opt-in to this by adding a fallback property in getStaticPaths. At build time, a static fallback page was generated so the first time a user visited a given URL, it would be served, and once the data was fetched, a new page would be served, and pushed to the CDN for subsequent loads. However, now we have the ability to avoid showing a fallback page at all and instead blocking the render until the data has been fetched by just adding fallback: 'blocking' to the returned object from getStaticPaths. *Before:* ` *After:* ` notFound support for getStaticProps and getServerSideProps The getStaticProps and getServerSideProps methods implemented a new boolean property: notFound. By adding it, your users will be redirected to your 404 page. ` redirect support for getStaticProps and getServerSideProps A redirect property was also added to both methods. Allowing you to redirect to either an internal or an external page, with the ability to also mark the redirect as permanent. ` Vercel's Next.js Analytics Although this is not related only with the framework, Vercel released Next.js Analytics, a new feature of the platform that collects analytics from real users by measuring Core Web Vitals and generating reports. Next.js Commerce If you are planning to spin up an e-commerce functionality on top of Next.js, you can just clone an all-in-one starter kit that includes all of the previously mentioned new features. Moving forward We are planning to release a series of Next.js articles in the upcoming weeks, including in-depth guides on how to create a Next.js application using all of the new features, including best practices and tips from the community. Stay tuned!...

The Future of Dates in JavaScript: Introducing Temporal cover image

The Future of Dates in JavaScript: Introducing Temporal

The Future of Dates in JavaScript: Introducing Temporal What is Temporaal? Temporal is a proposal currently at stage 3 of the TC39 process. It's expected to revolutionize how we handle dates in JavaScript, which has always been a challenging aspect of the language. But what does it mean that it's at stage 3 of the process? * The specification is complete * It has been reviewed * It's unlikely to change significantly at this point Key Features of Temporal Temporal introduces a new global object with a fresh API. Here are some important things to know about Temporal: 1. All Temporal objects are immutable 2. They're represented in local calendar systems, but can be converted 3. Time values use 24-hour clocks 4. Leap seconds aren't represented Why Do We Need Temporal? The current Date object in JavaScript has several limitations: * No support for time zones other than the user's local time and UTC * Date objects can be mutated * Unpredictable behavior * No support for calendars other than Gregorian * Daylight savings time issues While some of these have workarounds, not all can be fixed with the current Date implementation. Let's see some useful examples where Temporal will improve our lives: Some Examples Creating a day without a time zone is impossible using Date, it also adds time beyond the date. Temporal introduces PlainDate to overcome this. ` But what if we want to add timezone information? Then we have ZonedDateTime for this purpose. The timezone must be added in this case, as it also allows a lot of flexibility when creating dates. ` Temporal is very useful when manipulating and displaying the dates in different time zones. ` Let's try some more things that are currently difficult or lead to unexpected behavior using the Date object. Operations like adding days or minutes can lead to inconsistent results. However, Temporal makes these operations easier and consistent. ` Another interesting feature of Temporal is the concept of Duration, which is the difference between two time points. We can use these durations, along with dates, for arithmetic operations involving dates and times. Note that Durations are serialized using the ISO 8601 duration format ` Temporal Objects We've already seen some of the objects that Temporal exposes. Here's a more comprehensive list. * Temporal * Temporal.Duration` * Temporal.Instant * Temporal.Now * Temporal.PlainDate * Temporal.PlainDateTime * Temporal.PlainMonthDay * Temporal.PlainTime * Temporal.PlainYearMonth * Temporal.ZonedDateTime Try Temporal Today If you want to test Temporal now, there's a polyfill available. You can install it using: ` Note that this doesn't install a global Temporal object as expected in the final release, but it provides most of the Temporal implementation for testing purposes. Conclusion Working with dates in JavaScript has always been a bit of a mess. Between weird quirks in the Date object, juggling time zones, and trying to do simple things like โ€œadd a day,โ€ itโ€™s way too easy to introduce bugs. Temporal is finally fixing that. It gives us a clear, consistent, and powerful way to work with dates and times. If youโ€™ve ever struggled with JavaScript dates (and who hasnโ€™t?), Temporal is definitely worth checking out....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co