The web content provides a comprehensive tutorial on building a multi-step registration form using React Hook Form, React context, and React Router, with a focus on form structure, setup, and user experience enhancements like a step indicator.
Abstract
The tutorial guides developers through the process of creating a multi-step form in React, emphasizing the use of React Hook Form for form logic, React context for state management, and React Router for navigation between form steps. It covers the importance of structuring the form effectively, setting up the form with Context and Router, and implementing a step indicator for improved user experience. The form is designed to collect user information across several steps, culminating in a confirmation view that allows users to review and edit their input before submission. The article also touches on potential improvements for the form, such as enabling navigation via the stepper and handling draft data, and provides insights into testing strategies for multi-step forms.
Opinions
The author advocates for using separate form components for each step to achieve a clear separation of concerns and facilitate future changes.
React Hook Form is recommended for its ease of use and efficient handling of form state.
The use of a centralized store (React context) is suggested for persisting state between steps, with the caveat that more complex applications might benefit from a dedicated state management library like Redux.
The inclusion of a step indicator is considered essential for good user experience, providing clarity on the number of steps and the user's current progress.
The tutorial emphasizes the importance of testing each step component individually, focusing on user flow and validation, and references previous work on best practices for form testing with React Hook Form.
The author suggests that while abstraction is useful, over-abstracting can lead to rigidity in the codebase, making it less adaptable to new requirements.
Build a Multi-step Form With React Hook Form
A tutorial on how to build a multi-step form using React Hook Form.
The updated version of the post is available at claritydev.net.
Multistep forms a.k.a. wizard or funnel forms have a wide range of uses. They are most common when collecting different types of information into one data structure but are also useful for breaking down very large forms into the less user-intimidating multistep process. Some of the most popular examples of multistep forms are checkout and user registration forms.
In this post, we’ll build a basic multi-step registration form, which collects the user’s info and then presents it in a confirmation view, which can be reviewed and edited before the submission. It’d be noted that the form is greatly simplified and would probably have more fields in a real-life application, however, we’ll keep it simple for the sake of being easier to understand. Additionally, the focus is on the JS part and proper form styling is outside the scope of this tutorial. The final form looks like this (there is also CodeSandbox available):
Choosing the form structure
One of the hardest and the most important things when working with multistep forms is deciding on their structure. Ultimately the choice depends on the project requirements and what exactly the form is supposed to do. The first option is often to use one form component, inside which an active step is rendered. In this tutorial we will use a different approach — each step component will be a separate form, collecting the data from its fields and sending it to the central store on submit. This way we achieve a nice separation of concerns, making it easier to adapt to future requirement changes. For the form logic, we’ll use React Hook Form, which makes working with forms easier. For the central store, we’ll use React context, although for more complicated cases a separate state management library (e.g. Redux) could be a better fit. Lastly, each form step will have its own route, for which we’ll use React Router.
To summarise, the app flow is this:
Each step is a separate form with its own handlers and local state.
Pressing Next submits the data from the current step to the centralized store and redirects to the route for the next step.
In the final step, all the data is shown and users can edit any of the steps before submitting.
Setting up
As mentioned earlier, we’re using Context for persisting the state between the steps, and later show the form data in the preview before submitting. We’ll abstract the context providers and consumers into a separate file, for easier use.
Here we create an AppProvider, which handles the app state and related logic, and a custom hook useAppState. With this abstraction, we don't need to import AppStateContext into every component. Additionally, we can validate that the component that's calling this hook is used inside AppProvider.
Next, we add the routes to the app and create the components for the form steps, wrapping all of this into the AppProvider.
After that let’s set up the components for each step. Since they all will be form components, the code for each one will be quite similar. To abstract some repeated functionality, we create a separate Field component:
This component also handles the error message and, as a bonus, sets the label’s htmlFor attribute to match the id of the child input, making the input properly accessible. We just need to make sure that the input has an id. We'll also abstract the logic for the Form, Input, and Button components, although they are just wrappers for the native HTML elements with custom styling applied.
Now we can start creating the actual components for each step. They are deliberately simple, otherwise, it’s easy to get lost in all the details and edge cases. The form will have 4 steps:
Contact — get basic user details — name, email, and password
Education — let’s imagine that our form is used on some sort of education or career website, and we would like to know the user’s education details.
About — a step with a free text field for the users to provide more detailed info about themselves.
Confirm — this step is not used for entering details, but for previewing them and editing, if necessary.
Additionally, all the fields in the first step will be required and have a basic validation, to show how it’s usually done in React Hook Form.
React Hook Form internally handles the form state, so all we need to do is save it to our app state after it’s been validated.
The final step, Confirm, is slightly different since we need to display the data entered by the user. We’ll add a couple of section components, which we then compose to build the data presentation.
We could have gone further with abstractions and collected the data for display into a separate structure, which we’d then iterate over and render. However, going that much DRY with the code could prove problematic when we get new requirements, to which the code is not robust enough to adapt. One important rule to keep in mind is that it’s way easier to DRY the code than to un-DRY it.
Adding a step indicator
The form, although scantily styled, is functional, which we can confirm by filling in all the details and submitting them. As it currently stands, the users have no clear idea how many steps in total there are and where they are currently in the process. This is not a good UX and makes multistep forms more intimidating than they actually are. We’ll fix this by adding a step indicator a.k.a. the stepper. The Stepper component is basically a navigation for the form, but with the links disabled:
Now we can add this component to the App:
With this change, we have a fully working multistep form with a progress indicator.
Avenues for improvement
The form example in this tutorial is deliberately basic and concise. Several various improvements can be done here, and some of them will be discussed in future tutorials. A few examples of the most obvious improvements are:
The step indicator improves the UX quite a bit, however, once the user goes back to a step to edit it, they need to click Next until the last step is reached. This is quite annoying, particularly when going to the first step. To help with this we can enable navigation via the stepper.
If we enable navigation via the stepper, we’d also look into saving the form state when navigating via the stepper to make sure that the field validation is not bypassed.
Alternatively, we could disable validation for the required form fields and display the status of each step (if any required data is missing) in the stepper. This is helpful when we allow saving partial data as a draft.
Currently, if the user enters some data and decides to navigate away from the form, they lose all the progress. This becomes a problem when such navigation is done by mistake. In this case, we could add a prompt that would ask the user to confirm if they want to move to another page and lose all their progress.
The first two improvements and their implementation are discussed in the next post.
Testing the form
Even though multistep forms seem more complex than the usual, one-page forms, the approach to testing here is the same. My preferred way would be to test each step component separately with the basic user flow — fill in the fields, test that they are validated, and verify that correct data is sent on submit. Thinking about testing from the user’s perspective makes it easier. I have previously written about the form testing and some best practices, most of which could be also applied here.