React 19 - A Brief Look At Form Handling
Updated On
React 19 is finally here with it's beta release and it brings with it the support for custom elements (yayy! 🎉), better form handling with some new hooks and some improvements. An open source react compiler is also in the works - a version of which is being used by Instagram!
Installing React 19 (Beta) #
Vite can be used to test React 19 beta release locally. For that, you have to install the beta version using the following command after scaffolding a react project using vite:
npm i react@beta react-dom@beta
If you are using typescript, then you might have to do this in your package.json
file:
"dependencies": {
...
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta",
...
},
"overrides": {
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta"
}
Checking the version of React in the browser console can also be performed using:
// https://stackoverflow.com/questions/36994564/how-can-one-tell-the-version-of-react-running-at-runtime-in-the-browser#comment106830911_36994564
console.log(
__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values().next()["value"]["version"]
)
Form Actions in React #
Forms in React have always been not so easy to handle, which often leads to messy React code. Ultimately, folks have to resort to form handling libraries which only add more abstraction in the process.
One of the things I like about React 19 is the ability to handle form states. Of all the new hooks introduced for forms in React 19, I personally like the useActionState
hook used to handle basic forms.
The useActionState
hook exposes a function named submitAction
which can be passed to the action
attribute of the HTML <form>
element. It also accepts an initial state and a permalink value which I haven't tried yet, but the documentation states that it is for passing the state to another URL.
// useActionState: (fn, initialState, permalink?) => [state, submitAction, isPending]
const [state, submitAction, isPending] = useActionState(
async (previousState, formData) => {
try {
const response = await updateSomething(formData)
return {
data: response,
}
} catch (error) {
return {
error,
}
}
},
null // initial state
)
With the above function in place, you have to now pass the submitAction
function returned from the hook to the action
attribute of the form
element.
<form action={submitAction}>
<input type="text" name="name" />
<button type="submit" disabled={isPending}>Update</button>
</form>
The most helpful thing about this is that you get an isPending
state which is a boolean allowing you to determine the state of the submission without having to manage it through React's useState
. The same is true for form errors. If it's a validation error or a network error, you can send it with the response state that you are getting from the useActionState
hook and use it in your JSX. The UI will update itself automatically on the response of that hook.
Wrapping it in a Custom Hook #
In order to make it re-usable, you might want to wrap it in a custom hook which you can share between multiple forms. The name of the custom hook can be something like useFormHandler
.
import { useActionState } from 'react'
const useFormHandler = (callback, initialState = null, permalink = null) => {
const formState = useActionState(
async (previousState, formData) => {
try {
const response = await callback(formData)
return {
previousState,
data: response ? response : 'OK',
}
} catch (error) {
return {
previousState,
error,
}
}
},
initialState,
permalink && permalink
)
return {
formState
}
}
export default useFormHandler
The above custom hook accepts a callback function which is for you to handle the async network requests and validations. It accepts an initial state which you can pass if you want to, otherwise it defaults to null
. And lastly, a permalink which we discussed earlier. It returns the array returned by the useActionState
hook - [state, submitAction, isPending]
.
Use it by passing an async callback which handles the requests.
import useFormHandler from './hooks/formHandler'
const saveChanges = async (data) => {
return new Promise((resolve, reject) => {
try {
const res = data.get("name")
// ... do something with the data
resolve(res)
} catch (error) {
reject(error.message)
}
})
}
const { formState } = useFormHandler(saveChanges)
const [state, submitAction, isPending] = formState
The use case for creating a custom hook is to add more validations and checks for the form data and to standardize the response state received from the useActionState
hook.
Some more hooks
React 19 also introduced some more hooks such as useFormStatus
and useOptimistic
but I don't have a clear idea on what their best use case can be. Let's hope that with the introduction of newer hooks, the hooks land doesn't become cluttered and murkier than before.
It’s a funny conundrum. Why do we refactor? Because the code got too complicated. So we simplify it. And why do we simplify it? So we can add more to it over time and make it more complex again. - Jim Nielsen