diff --git a/src/content/reference/react-dom/components/form.md b/src/content/reference/react-dom/components/form.md index 10e9c67940e..4904c663470 100644 --- a/src/content/reference/react-dom/components/form.md +++ b/src/content/reference/react-dom/components/form.md @@ -38,21 +38,27 @@ To create interactive controls for submitting information, render the [built-in `
` supports all [common element props.](/reference/react-dom/components/common#common-props) -[`action`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action): a URL or function. When a URL is passed to `action` the form will behave like the HTML form component. When a function is passed to `action` the function will handle the form submission in a Transition following [the Action prop pattern](/reference/react/useTransition#exposing-action-props-from-components). The function passed to `action` may be async and will be called with a single argument containing the [form data](https://developer.mozilla.org/en-US/docs/Web/API/FormData) of the submitted form. The `action` prop can be overridden by a `formAction` attribute on a ` + +
- ); } ``` -In lieu of using hidden form fields to provide data to the `
`'s action, you can call the `bind` method to supply it with extra arguments. This will bind a new argument (`productId`) to the function in addition to the `formData` that is passed as an argument to the function. +Instead of using a hidden form field, call the `bind` method to pass an extra argument to the Server Function. This binds `productId` as an argument before the `formData` that React passes to the function. ```jsx [[1, 8, "bind"], [2,8, "productId"], [2,4, "productId"], [3,4, "formData"]] import { updateCart } from './lib.js'; function AddToCart({productId}) { async function addToCart(productId, formData) { - "use server"; - await updateCart(productId) + 'use server'; + await updateCart(productId); } const addProductToCart = addToCart.bind(null, productId); return ( @@ -155,9 +171,10 @@ function AddToCart({productId}) { } ``` -When `` is rendered by a [Server Component](/reference/rsc/use-client), and a [Server Function](/reference/rsc/server-functions) is passed to the ``'s `action` prop, the form is [progressively enhanced](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). +--- + +### Displaying a pending state during form submission {/*display-a-pending-state-during-form-submission*/} -### Display a pending state during form submission {/*display-a-pending-state-during-form-submission*/} To display a pending state when a form is being submitted, you can call the `useFormStatus` Hook in a component rendered in a `` and read the `pending` property returned. Here, we use the `pending` property to indicate the form is submitting. @@ -165,14 +182,14 @@ Here, we use the `pending` property to indicate the form is submitting. ```js src/App.js -import { useFormStatus } from "react-dom"; -import { submitForm } from "./actions.js"; +import { useFormStatus } from 'react-dom'; +import { submitForm } from './actions.js'; function Submit() { const { pending } = useFormStatus(); return ( ); } @@ -191,31 +208,33 @@ export default function App() { ``` ```js src/actions.js hidden -export async function submitForm(query) { - await new Promise((res) => setTimeout(res, 1000)); +export async function submitForm(formData) { + await new Promise((res) => setTimeout(res, 1000)); } ``` -To learn more about the `useFormStatus` Hook see the [reference documentation](/reference/react-dom/hooks/useFormStatus). +To learn more about the `useFormStatus` Hook, see the [reference documentation](/reference/react-dom/hooks/useFormStatus). + +--- ### Optimistically updating form data {/*optimistically-updating-form-data*/} + The `useOptimistic` Hook provides a way to optimistically update the user interface before a background operation, like a network request, completes. In the context of forms, this technique helps to make apps feel more responsive. When a user submits a form, instead of waiting for the server's response to reflect the changes, the interface is immediately updated with the expected outcome. For example, when a user types a message into the form and hits the "Send" button, the `useOptimistic` Hook allows the message to immediately appear in the list with a "Sending..." label, even before the message is actually sent to a server. This "optimistic" approach gives the impression of speed and responsiveness. The form then attempts to truly send the message in the background. Once the server confirms the message has been received, the "Sending..." label is removed. - ```js src/App.js -import { useOptimistic, useState, useRef } from "react"; -import { deliverMessage } from "./actions.js"; +import { useOptimistic, useState, useRef } from 'react'; +import { deliverMessage } from './actions.js'; function Thread({ messages, sendMessage }) { const formRef = useRef(); async function formAction(formData) { - addOptimisticMessage(formData.get("message")); + addOptimisticMessage(formData.get('message')); formRef.current.reset(); await sendMessage(formData); } @@ -248,17 +267,17 @@ function Thread({ messages, sendMessage }) { export default function App() { const [messages, setMessages] = useState([ - { text: "Hello there!", sending: false, key: 1 } + { text: 'Hello there!', sending: false, key: 1 } ]); async function sendMessage(formData) { - const sentMessage = await deliverMessage(formData.get("message")); + const sentMessage = await deliverMessage(formData.get('message')); setMessages((messages) => [...messages, { text: sentMessage }]); } return ; } ``` -```js src/actions.js +```js src/actions.js hidden export async function deliverMessage(message) { await new Promise((res) => setTimeout(res, 1000)); return message; @@ -267,21 +286,22 @@ export async function deliverMessage(message) { -[//]: # 'Uncomment the next line, and delete this line after the `useOptimistic` reference documentation page is published' -[//]: # 'To learn more about the `useOptimistic` Hook see the [reference documentation](/reference/react/useOptimistic).' +To learn more about the `useOptimistic` Hook, see the [reference documentation](/reference/react/useOptimistic). + +--- ### Handling form submission errors {/*handling-form-submission-errors*/} -In some cases the function called by a ``'s `action` prop throws an error. You can handle these errors by wrapping `` in an Error Boundary. If the function called by a ``'s `action` prop throws an error, the fallback for the error boundary will be displayed. +To handle errors thrown by a function passed to a ``'s `action` prop, wrap the form in an Error Boundary. React displays the boundary's fallback when the function throws. ```js src/App.js -import { ErrorBoundary } from "react-error-boundary"; +import { ErrorBoundary } from 'react-error-boundary'; export default function Search() { function search() { - throw new Error("search error"); + throw new Error('search error'); } return ( ); } - ``` ```json package.json hidden @@ -305,14 +324,15 @@ export default function Search() { "react-scripts": "^5.0.0", "react-error-boundary": "4.0.3" }, - "main": "/index.js", - "devDependencies": {} + "main": "/index.js" } ``` -### Display a form submission error without JavaScript {/*display-a-form-submission-error-without-javascript*/} +--- + +### Displaying a form submission error without JavaScript {/*display-a-form-submission-error-without-javascript*/} Displaying a form submission error message before the JavaScript bundle loads for progressive enhancement requires that: @@ -320,80 +340,159 @@ Displaying a form submission error message before the JavaScript bundle loads fo 1. the function passed to the ``'s `action` prop be a [Server Function](/reference/rsc/server-functions) 1. the `useActionState` Hook be used to display the error message -`useActionState` takes two parameters: a [Server Function](/reference/rsc/server-functions) and an initial state. `useActionState` returns two values, a state variable and an action. The action returned by `useActionState` should be passed to the `action` prop of the form. The state variable returned by `useActionState` can be used to display an error message. The value returned by the Server Function passed to `useActionState` will be used to update the state variable. +Define the Server Function in a separate file with the [`'use server'`](/reference/rsc/use-server) directive. It receives the previous state followed by the submitted `FormData`: + +```js +// actions.js +'use server'; + +import { signUpNewUser } from './api.js'; + +export async function signup(previousState, formData) { + const email = formData.get('email'); + try { + await signUpNewUser(email); + return null; + } catch (error) { + return error.message; + } +} +``` + +In a Client Component, pass the Server Function to `useActionState`. Pass the returned Action to the form's `action` prop and render the returned state: + +```js +// Signup.js +'use client'; + +import { useActionState } from 'react'; +import { signup } from './actions.js'; + +export default function Signup() { + const [message, signupAction] = useActionState(signup, null); + return ( + + + + + {message &&

{message}

} +
+ ); +} +``` + +If the form is submitted before JavaScript loads, React includes the Server Function's returned error message in the server-rendered response. + +--- + +### Preserving form values after submission {/*preserve-form-values-after-submission*/} + +Submitting a form with a URL `action` clears its input state. React mirrors this behavior when `action` is a function by resetting the form's [uncontrolled fields](/reference/react-dom/components/input#reading-the-input-values-when-submitting-a-form) after the Action succeeds. When a Server Function progressively enhances a form, this keeps its behavior consistent before and after JavaScript loads. [Inputs controlled with state](/reference/react-dom/components/input#controlling-an-input-with-a-state-variable) are not cleared. + +#### Restore fields with `useActionState` {/*with-useactionstate*/} + +Pass the Action returned by [`useActionState`](/reference/react/useActionState) to the `action` prop. Return the values you want to keep from your Action, and pass them to each field's `defaultValue`. The automatic form reset restores those default values instead of clearing the fields. ```js src/App.js -import { useActionState } from "react"; -import { signUpNewUser } from "./api"; - -export default function Page() { - async function signup(prevState, formData) { - "use server"; - const email = formData.get("email"); - try { - await signUpNewUser(email); - alert(`Added "${email}"`); - } catch (err) { - return err.toString(); - } - } - const [message, signupAction] = useActionState(signup, null); +import { useActionState } from 'react'; +import { submitForm } from './api.js'; + +export default function EditForm() { + const [state, dispatchAction, isPending] = useActionState(submitForm, { + title: 'My draft', + }); + return ( - <> -

Signup for my newsletter

-

Signup with the same email twice to see an error

-
- - - - {!!message &&

{message}

} -
- +
+ + +
); } ``` ```js src/api.js hidden -let emails = []; - -export async function signUpNewUser(newEmail) { - if (emails.includes(newEmail)) { - throw new Error("This email address has already been added"); - } - emails.push(newEmail); +export async function submitForm(previousState, formData) { + await new Promise((res) => setTimeout(res, 1000)); + return { + title: formData.get('title'), + }; } ```
-Learn more about updating state from a form action with the [`useActionState`](/reference/react/useActionState) docs + -### Handling multiple submission types {/*handling-multiple-submission-types*/} +#### Choosing how to manage form values {/*choosing-how-to-manage-form-values*/} + +Choose an approach based on what should happen after submission: + +* **Preserve selected values with `useActionState`.** The example above returns the submitted title after every submission. To preserve values only when validation fails, return the submitted `FormData` in the error state and use it to set each field's `defaultValue`. With a Server Function, React can include those values in the server response before JavaScript loads. + +* **Keep every value with `onSubmit`.** Call `e.preventDefault()`, then run the Action inside [`startTransition`](/reference/react/useTransition). Calling `preventDefault()` prevents the function passed to the form's `action` prop from running for that submission, so React does not automatically reset the form. + +* **Reset fields at a specific point.** Call the form element's [`reset()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset) method to immediately reset uncontrolled fields to their default values. To schedule the same reset inside an Action or Transition, call [`requestFormReset`](/blog/2024/12/05/react-19#form-actions) from `react-dom`. -Forms can be designed to handle multiple submission actions based on the button pressed by the user. Each button inside a form can be associated with a distinct action or behavior by setting the `formAction` prop. +* **Reset the fields and component state.** Change the [`key`](/learn/preserving-and-resetting-state#resetting-a-form-with-a-key) on the component that renders the form. React recreates the component and its DOM, so its fields and local state both start over. -When a user taps a specific button, the form is submitted, and a corresponding action, defined by that button's attributes and action, is executed. For instance, a form might submit an article for review by default but have a separate button with `formAction` set to save the article as a draft. + + +--- + +### Handling multiple submission types {/*handling-multiple-submission-types*/} + +A form can have more than one submit button, each running a different Action. A button without `formAction` runs the form's `action`; a button with `formAction` runs its own Action instead. For example, the form below publishes an article by default, but its **Save draft** button stores the current content without publishing it: ```js src/App.js -export default function Search() { +import { useActionState } from 'react'; + +export default function ArticleForm() { + // Hold the saved draft in state so the textarea keeps its content after saving + const [formState, dispatchFormState] = useActionState((state, payload) => { + const content = payload.data.get('content'); + switch (payload.type) { + case 'save': + alert(`Your draft of '${content}' was saved!`); + // Keep the submitted content as the current draft + return payload.data; + case 'publish': + alert(`'${content}' was published!`); + // Reset the form + return new FormData(); + default: + return state; + } + }, new FormData()); + function publish(formData) { - const content = formData.get("content"); - const button = formData.get("button"); - alert(`'${content}' was published with the '${button}' button`); + dispatchFormState({ + type: 'publish', + data: formData, + }); } function save(formData) { - const content = formData.get("content"); - alert(`Your draft of '${content}' has been saved!`); + dispatchFormState({ + type: 'save', + data: formData, + }); } return (
-