In this article, we will explore two latest React features: Form Actions and the useFormStatus
hook, to create and manage forms in a better way. Note that, at the time of writing this article, these features are only available in React's Canary and Next.js App Router. Let's jump in!
Use Form Action
The introduction of Form Actions has made it easier for us to work with forms in React. Below is an example of a form using the Form Action:
'use client';
import { register } from '@server/actions/user';
import SubmitButton from '@component/SubmitButton';
export default function RegisterForm() {
async function handleSubmit(formData: FormData) {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
if (!email || !password) {
// Show missing required fields alert and return
return console.log('Missing required fields');
}
const { error } = await register(email, password);
// Show error message as alert and return
if (error) return console.log(error.message);
}
return (
<form action={handleSubmit}>
<label htmlFor='email'>Email</label>
<input type='email' id='email' name='email' />
<label htmlFor='password'>Password</label>
<input type='password' id='password' name='password' />
<SubmitButton />
</form>
);
}
In the above code, we have a RegisterForm
client component. We've made it a client component so that we can show data validation and submission error alerts.
Within the component, we have a handleSubmit
function, and we return a form
element with two input
elements. This is an extended form
element that allows us to pass a function to the action
prop. In our case, we pass the handleSubmit
function.
The handleSubmit
function now works as a Form Action. By default, a Form Action is called with the formData
argument, which contains the data of the submitted form.
Within the handleSubmit
function, we access the form data by using the formData.get
method and passing an argument to it. Note that the arguments must be the value of the name
attributes provided in the input
elements.
Finally, after extracting the data, we validate and call the register
function with it. The register
function is a server action that runs on the server and registers the user.
Use ‘useFormStatus’ hook
The other significant part of a form is the submit button. Below is an example of how we leverage the latest React features to create a submit button for our form:
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type='submit' disabled={pending}>
{pending ? 'Loading...' : 'Submit'}
</button>
);
}
In the above code, we have a SubmitButton
client component. We made it a client component so that we can use the useFormStatus
hook. Within the component, we destructure the result of the useFormStatus
hook and get the pending
property out of it.
The value of the pending
property is a boolean, which is true
when the form submission starts and changes to false
when the form submission ends. We use the pending
property to disable the button
during the submission and change the button
text based on the status.
Note that for the useFormStatus
hook to work, we must put the SubmitButton
component inside the form
element.
That's it! This approach eliminates the need to manage form and submission status states by leveraging the latest React features. However, in an advanced form with features like checkboxes, we still need to use states to get the value of the selected elements. That is fine, though, as we can combine both approaches and get the best of both worlds.