React Hook Form vs React 19

Well this is awkward. Just as I was finishing and publishing a video on why to use React Hook Form instead of React, React 19 comes out with a massive revamp of form handling. Let’s break it down.

Matt Burgess
9 min readJan 1, 2025

One of the big change in React 19 is how it handles forms and form submission, and the implications of that for dealing with forms in general. Some of this makes React Hook Form less of a necessary choice, essentially pushing back that requirement.

We’ll start with actions.

Working with Actions

NextJS has for a long time been promoting the concept of “server actions”. These are simple async functions that handle something – typically data fetching or sending. They’re intended to be run on the server, not in the client, and have directives to do so.

React 19 extends this principle, but the actions aren’t just “Server” actions, but actions in general. Actions for React 19 have a particular method signature:

const SaveAction = async (previousState, formData) => {
//
}

This formData argument is particularly interesting. This isn’t some arbitrary data, but an actual HTML API FormData object. This means it has a bunch of specific methods, like formData.get(“email”). It’s not just a standard pojo where you can do formData.email.

This also means that if you get that value you have to convert it in order to send it as a JSON payload. By default it will send as a multipart/form-data format, not as json. This can be fixed in one line:

const payload = Object.fromEntries(formData.entries())

However it’s an easy thing to miss and not realise that you have to do it.

What that action actually does is up to you. HTTP requests of some kind are pretty typical, so let’s just assume that.

const RegisterUserAction = async (previousState, formData) => {
const fde = formData.entries();
const payload = Object.fromEntries(fde);
const { data } = await axios.post(payload);

return data;
}

For the record I’m just using axios because you end up with a much shorter example. Obviously fetch would work the same.

This action function is not so different from what you might do as a handleSubmit function that you use as an onSubmit event. The only real difference is that you don’t get or need the event as the argument. Obviously there’s not too much to say, so let’s see it in context

Form Actions

You can now use the action functions as the action prop of a form.

<form action={submitForm} >

This might look strange to anyone only used to JavaScript forms, but frankly form action is how we submitted forms back in the day. It was the url we submitted a form to. We even had a form method. Wild stuff.

Anyway, you’ll note that this doesn’t have an event. It isn’t onSubmit, or connected to the button as an onClick. It’s an “action”, a distinctly different way of handling things.

A nice point to add here is that if you have an action like this, React will handle two things about the form that are convenient. One is that you don’t need to use event.preventDefault(), that’s handled by React. Another is that you don’t need to reset the form after submission, React does that as well.

New Form Hooks

The actions themselves can be used in a form directly, but you get some more functionality by wrapping them in some of the new hooks. The most relevant is `useActionState`. This used to be called `useFormState`, but it was decided that the functionality was not unique to forms, so it’s using the more general term.

The useActionState function wraps the action directly, and returns an tuple of values.

const [response, submitAction, pending] = useActionState(RegisterUserAction)

What makes this particularly useful is that you can see from it that you have a response property. Some people name this as “error”, because it can return errors. I don’t get that though, as it contains whatever you return from the action, so there’s no reason to call it error.

To explain that further, if we look at at the earlier example we can see something kind of awkward.

const RegisterUserAction = async (previousState, formData) => {
const fde = formData.entries();
const payload = Object.fromEntries(fde);
const { data } = await axios.post(payload);

return data;
}

export default RegisterForm() {
return (
<form action={RegisterUserAction}>
//
</form>
);
}

We have this form and we have this action, and they’re bound together. Our action returns a newly created user, with an ID, etc. But we have no way of getting access to that object. The whole thing is swallowed. The action is used, but never returned.

The useActionState function lets us get access to this stuff.

const RegisterUserAction = async (previousState, formData) => {
// as above
return data;
}

export default RegisterForm() {
const [user, submitAction] = useActionState(RegisterUserAction);

if(user && user !instanceOf Error) {
window.location = “/dashboard”;
}

return (
<form action={RegisterUserAction}>
//
</form>
);
}

This isn’t great, and has no error handling, etc, but you get the idea. The first entry in the tuple is the response of our action, and it’s undefined by default. Though useActionState actually takes a second argument, which is the default value. Useful for populating with something like an empty array, or if the structure returned has multiple props you don’t want to leave empty.

Another entry in the growing list of React hooks is useOptimistic. This is intended for forms where you want to optimistically update. To be a bit less circular, this is where you want to update the UI with the assumption that the request worked, regardless of whether it actually did or not, and then rollback in the unlikely event of an error. This article isn’t intended to go into detail about these changes so let me just say for the record I don’t much like the pattern and it requires a bit of dicking around with the structure of the form action itself to make it work. It’s not like useActionState where you just wrap an existing action.

The last one to talk about is useFormStatus. You can think about this as not dissimilar to useFormContext in React Hook Form if that didn’t return anything other than isSubmitting. The purpose of this hook is to allow you to access details of the form state from within a nested component, for example.

New Field Handling

Probably the best bit of React 19's form handling is how it manages input fields. If you’re not familiar, in React if you just stuff an input tag in the JSX you end up with React doing this fun thing where it sees an input, then re-renders, which wipes the input.

This means you need to do a very standard pattern – I did it in my previous article – to maintain a working element, which is to make it a “controlled input”.

export default function MyInput() {
const [email, setEmail] = useState(‘’);

return (
<input name=”email” type=”email” value={email}
onChange={(e) => setEmail(e.target.value)}
/>
);
)

This is just for one field, so you can see how it would balloon out with multiple fields, tracking multiple bits of state. This was specifically the starting point of my article previously.

React 19 by contrast considers input values “stable” and does not re-render them in its render cycle. This makes the markup much more reasonable.

export default function MyInput() {
return <input name=”email” type=”email” />;
}

There’s no state to manage, and this is considered to not be React’s problem to manage.

If you’re wondering how submission works, given that you don’t have any state to send, that’s a valid question. The fact is, there’s still “state”. The browser still knows what the state is, is still keeping track of the fields in the form. It uses standard web APIs to keep the fields up to date, and standard web APIs to get the form data to submit.

This is really just React getting out of the way the web has always worked, and I am 100% here for it. 10/10, fantastic.

So do you need React Hook Form anymore?

You might not, is the short answer. But there’s a key thing we haven’t mentioned yet that throws a spanner in the wrench: validation.

How does React 19 handle form validation? It doesn’t. It has absolutely no support for it, beyond standard HTML form required fields and pattern. It has no mechanism for tracking field errors, or preventing invalid submission.

There’s a chance that React 19 forms are sufficient for you, especially if you have a simple form like a search. But most real-world forms are going to require at least a little validation and at that point you’re likely to want something.

There are a couple of options here. One is to just keep using something like RHF. I think that’s valid. Personally that’s my choice at least for now. The other is a more hybrid approach of using a tool like Zod to generate a schema and then manually checking against that schema.

It’s worth noting that because we’re using the action attribute, the onSubmit event is wide open, making something like this completely valid.

const handleSubmit = (e) => {
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
const result = registerUserSchema.safeParse(data);

if(!result.success) {
e.preventDefault();
// handle errors here.
}
}

<form action={RegisterUser} onSubmit={handleSubmit}>

Something roughly like this – with way better error handling – should work for some cases.

But with this all said, I’ll still be using React Hook Form at least for a while. And here’s where we get to the issue.

This Too Shall Pass

There is a current issue in that there is React 19 and then there is NextJS. For the most part, NextJS 15 should be completely compatible with React 19, and they’re designed to work together. However, there’s an inconsistency here. Both React and NextJS offer “actions”, with Next using Server Actions while React just offers Actions.

These are similar in intent, but have a slightly different method signature. React Actions require a first previousState argument and then the form data, while Next Server actions just take the form data.

There is a glaring design issue here, and it’s all on React 19. For a start, React Actions can only be used with forms. They explicitly demand formData of the type FormData. They are not just “Actions”. They are Form Actions. They have no flexibility in how they are used or how they are executed.

What’s particularly strange is that one of the few hooks built around these actions — useActionState– was originally called useFormState but they changed it so that it was more generic, yet the action is still entirely form-specific.

The second glaring issue is that the arguments used by React Actions and Next Server Actions are not aligned. If React had the formData argument first, and the previousState argument second that would be entirely reasonable, and would make previousState an optional argument.

Not only would this align with an existing dominant implementation for server/form actions, but it’s just basic software principles: the arguments should be in order of importance, and the form data is always going to be more important than the unrelated ui state.

React Hook Form in a React 19 world

This is only going to get more complex. Best case scenario is that we can muddle through until some of these APIs align. This is the case for forms, but it’s also the case for server components themselves, another feature React 19 expands on. (And out of scope of this article.)

Until there’s a RHF version that integrates better with R19's approach. Or until another library that handles form lifecycles as well. Until patterns emerge that improve React 19 inherent handling of validation, we’re probably stuck with a sub-optimal implementation of React Hook Form with React 19.

It’s worth being clear-eyed on this. Nothing changes. We’ll use RHF in exactly the same way we currently do. We just don’t get the benefits of simpler internal state in React 19, and have to keep our inputs controlled. While this is unfortunate, it’s certainly not the end of the world. A better way we can’t quite use yet is hardly a first for web development.

For myself I’ve built a few small forms ditching React Hook Form entirely – something I never would have done before. But for larger and more complex forms like multi-step ones, or ones with a lot of validation I’m sticking with React Hook Form.

--

--

Matt Burgess
Matt Burgess

Written by Matt Burgess

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.

Responses (3)