React 19 - A Brief Look At Form Handling

Murtuzaali Surti
Murtuzaali Surti

• 4 min read

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


Static Sites Are Good

Previous

Creating My First Web Component: The <back-to-top> Button

Next