In my previous blog, we had discussed using server actions with our forms and managing the loading state with the new useFormStatus hook from react-dom. In this one, we are going to explore another experimental hook from react-dom: useFormState.
Concisely, useFormState is a hook to which we provide a function to manipulate form data. The function provides us with two values, the first being the form data value or the manipulated data we get from our provided function. As we know, when developing NextJS apps, we prefer to have our code on the server side. This hook is beneficial since the function we provide to it will be a server function. More on that later.
Using useFormState in your Code
As I have mentioned earlier, this is an experimental hook by react-dom, meaning it will not be available with our usual npm install package installing command. For this, we will have to run the command instruction in our terminal meant to install the experimental version of react and react-dom:
npm install react@experimental react-dom@experimental
After installation of experimental packages, your package.json should have the dependencies as follows:
Once this has been completed, a file should also be created to inform your project that the experimental utilities will be used if typescript is being used in your project:
After this, you should be able to import useFormState in your files and use them accordingly. //@ts-ignore” may need to be added right above the import as TypeScript will not recognize this experimental import, but it will still function as intended.
//@ts-ignore
import { useFormState } from "react-dom";
Creating a Simple Form
Let’s create a simple form that accepts a name and an age.
<form action={onSubmit} className="form-control py-3 border-primary border-3"> <Input inputName="username" placeholder="Enter Name" /> <Input inputName="age" placeholder="Enter Age" /> <SaveButton /> </form>
We will be using the “name” property of HTML’s input tag, which is given value here in our Input component by the “inputName” property, for picking data from the form inputs.
const Input = ({ inputName, placeholder }: { inputName: string; placeholder: string; }) => { return ( <div className="my-2"> <input className="form-control" name={inputName} type="text" placeholder={placeholder} /> </div> ); };
This is the output of our basic form so far:
As for the submit button, this is where we will be using an aspect of useFormStatus, another experimental hook I mentioned earlier, that is the loading state. It is a Boolean value by the name “pending,” which we get from this hook. We can use this for disabling the submit button to avoid multiple form submit calls or to change the text on the button like we have done here:
//@ts-ignore import { useFormStatus } from "react-dom"; const SaveButton = () => { const { pending: isLoading } = useFormStatus(); return ( <div className="my-3"> <button disabled={isLoading} className="btn btn-warning form-control" type="submit" > {isLoading ? "Saving..." : "Save"} </button> </div> ); };
More on this in my previous blog post on this hook.
As for our form, we will use the form’s action attribute for our submit action. But first, let’s take a look at the action we create for the purpose of form submission.
const [{ username, age, error }, updateProfile] = useFormState( saveProfile, { username: "", age: "", error: {}, } );
As seen in the above code snippet, useFormState takes two values. The first is the action we want to perform when submitting the form, and the second is the initial state values of the form. It then returns two values in the form of an array, as we see with the useState hook from React. When we execute our submit action, we receive the updated value as the first value, and the second value is a function that mirrors our created function, the “saveProfile” function. We will utilize this new function, “updateProfile,” for our form submission. When the form is submitted, we will get the updated values again in the first value of the returned array, which I have deconstructed here for convenience.
Form Submit Action
Now let’s take a look at the server action that we passed to our experimental new hook:
"use server"; import { Profile} from "./types"; export const saveProfile: (_prevState: Profile, profile: Profile) => void = ( _prevSate, profile ) => profile;
As we can observe from the top of this file, the file is a server file, and therefore, it will execute its action on the server side rather than the client side. This is basically the main reason for the use of this process. Now if we look at our function, it is accepting two parameters, but we only should be passing one parameter value, which would be our form data. So where did this extra parameter come from? It is actually due to the useFormState hook.
Earlier, I stated that we pass a function to the useFormState hook and, in return, receive a value and a function that resembles a copy of the function passed to the hook. So the extra parameter is actually from the new “updateProfile” function we have taken from the hook. And as seen from the first parameter’s name, this is the extra parameter which has the previous state data of our form, since we do not have a need for this, I have appended an underscore to its name to denote it as an unused variable.
Now Let’s Use This With our Form
To do so, we just need to pass the new function we got from the useFormState hook to the form’s action property. By passing FormData of the form with the details of the form to our function, we can retrieve the data using the get function of the FormData.
const username = (formData.get("username") as string) || ""; const age = (formData.get("age") as string) || "";
Let’s add some validation to our form submit function:
export const saveProfile: (_prevState: Profile, formData: FormData) => Profile = ( _prevSate, formData ) => { const username = (formData.get("username") as string) || ""; const age = (formData.get("age") as string) || ""; const invalidName = username.length && username.length < 4; const invalidAge = age.length && parseInt(age) < 18; let error: ProfileError = {}; if (!username) error.username = "Name cannot be empty"; if (!age) error.age = "Age cannot be empty"; if (invalidName) error.username = "Username must be at least 4 characters"; if (invalidAge) error.age = "Age must be above 18 years"; if (username && !invalidName) error.username = ""; if (age && !invalidAge) error.age = ""; return ({ username, age, error }); };
Typically, when a user enters values into form input fields and clicks on the submit button, we expect the submit action to be on the client side as these actions are performed on the client device. But with the useFormState we have managed to move this logic to the server side as a server action. We can add more complex validations than what we see above using regex or service API for more checks if required.
Adding these error messages to our form along with their respective input fields:
<form action={updateProfile} className="form-control py-3 border-primary border-3"> <Input inputName="username" placeholder="Enter Name" /> <div className="text-danger fs-6">{error.username}</div> <Input inputName="age" placeholder="Enter Age" /> <div className="text-danger fs-6">{error.age}</div> <SaveButton /> </form>
Let’s see the result of our form with the error validation:
Empty form submission:
Invalid Username input:
Invalid Age input:
Successful form submission:
Conclusion
In conclusion, mastering the useFormState hook in React enables efficient form management, offering easy access to form data and dynamic state tracking. Experiment with its features to enhance your form-building skills and deliver seamless user experiences.