Skip to content

Commit

Permalink
Support React 19 in App and Pages router (#65058)
Browse files Browse the repository at this point in the history
Closes NEXT-3218

---------

Co-authored-by: Jiachi Liu <inbox@huozhi.im>
  • Loading branch information
eps1lon and huozhi committed May 7, 2024
1 parent 1f598bc commit 2c31c79
Show file tree
Hide file tree
Showing 547 changed files with 393,733 additions and 453,016 deletions.
Expand Up @@ -358,10 +358,10 @@ export default async function createsUser(formData) {
}
```

Once the fields have been validated on the server, you can return a serializable object in your action and use the React [`useFormState`](https://react.dev/reference/react-dom/hooks/useFormState) hook to show a message to the user.
Once the fields have been validated on the server, you can return a serializable object in your action and use the React [`useActionState`](https://react.dev/reference/react-dom/hooks/useActionState) hook to show a message to the user.

- By passing the action to `useFormState`, the action's function signature changes to receive a new `prevState` or `initialState` parameter as its first argument.
- `useFormState` is a React hook and therefore must be used in a Client Component.
- By passing the action to `useActionState`, the action's function signature changes to receive a new `prevState` or `initialState` parameter as its first argument.
- `useActionState` is a React hook and therefore must be used in a Client Component.

```tsx filename="app/actions.ts" switcher
'use server'
Expand All @@ -385,20 +385,20 @@ export async function createUser(prevState, formData) {
}
```

Then, you can pass your action to the `useFormState` hook and use the returned `state` to display an error message.
Then, you can pass your action to the `useActionState` hook and use the returned `state` to display an error message.

```tsx filename="app/ui/signup.tsx" switcher
'use client'

import { useFormState } from 'react-dom'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'

const initialState = {
message: '',
}

export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
const [state, formAction] = useActionState(createUser, initialState)

return (
<form action={formAction}>
Expand All @@ -417,15 +417,15 @@ export function Signup() {
```jsx filename="app/ui/signup.js" switcher
'use client'

import { useFormState } from 'react-dom'
import { useActionState } from 'react'
import { createUser } from '@/app/actions'

const initialState = {
message: '',
}

export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
const [state, formAction] = useActionState(createUser, initialState)

return (
<form action={formAction}>
Expand Down Expand Up @@ -739,7 +739,7 @@ export async function createTodo(prevState, formData) {
> **Good to know:**
>
> - Aside from throwing the error, you can also return an object to be handled by `useFormState`. See [Server-side validation and error handling](#server-side-validation-and-error-handling).
> - Aside from throwing the error, you can also return an object to be handled by `useActionState`. See [Server-side validation and error handling](#server-side-validation-and-error-handling).
### Revalidating data
Expand Down Expand Up @@ -1002,5 +1002,5 @@ For more information on Server Actions, check out the following React docs:
- [`"use server"`](https://react.dev/reference/react/use-server)
- [`<form>`](https://react.dev/reference/react-dom/components/form)
- [`useFormStatus`](https://react.dev/reference/react-dom/hooks/useFormStatus)
- [`useFormState`](https://react.dev/reference/react-dom/hooks/useFormState)
- [`useActionState`](https://react.dev/reference/react-dom/hooks/useActionState)
- [`useOptimistic`](https://react.dev/reference/react/useOptimistic)
Expand Up @@ -29,7 +29,7 @@ The examples on this page walk through basic username and password auth for educ

### Sign-up and login functionality

You can use the [`<form>`](https://react.dev/reference/react-dom/components/form) element with React's [Server Actions](/docs/app/building-your-application/rendering/server-components), [`useFormStatus()`](https://react.dev/reference/react-dom/hooks/useFormStatus), and [`useFormState()`](https://react.dev/reference/react-dom/hooks/useFormState) to capture user credentials, validate form fields, and call your Authentication Provider's API or database.
You can use the [`<form>`](https://react.dev/reference/react-dom/components/form) element with React's [Server Actions](/docs/app/building-your-application/rendering/server-components), [`useFormStatus()`](https://react.dev/reference/react-dom/hooks/useFormStatus), and [`useActionState()`](https://react.dev/reference/react/useActionState) to capture user credentials, validate form fields, and call your Authentication Provider's API or database.

Since Server Actions always execute on the server, they provide a secure environment for handling authentication logic.

Expand Down Expand Up @@ -200,16 +200,16 @@ export async function signup(state, formData) {
}
```

Back in your `<SignupForm />`, you can use React's `useFormState()` hook to display validation errors to the user:
Back in your `<SignupForm />`, you can use React's `useActionState()` hook to display validation errors to the user:

```tsx filename="app/ui/signup-form.tsx" switcher highlight={7,15,21,27-36}
'use client'

import { useFormState } from 'react-dom'
import { useActionState } from 'react'
import { signup } from '@/app/actions/auth'

export function SignupForm() {
const [state, action] = useFormState(signup, undefined)
const [state, action] = useActionState(signup, undefined)

return (
<form action={action}>
Expand Down Expand Up @@ -248,11 +248,11 @@ export function SignupForm() {
```jsx filename="app/ui/signup-form.js" switcher highlight={7,15,21,27-36}
'use client'

import { useFormState } from 'react-dom'
import { useActionState } from 'react'
import { signup } from '@/app/actions/auth'

export function SignupForm() {
const [state, action] = useFormState(signup, undefined)
const [state, action] = useActionState(signup, undefined)

return (
<form action={action}>
Expand Down Expand Up @@ -293,7 +293,8 @@ You can also use the `useFormStatus()` hook to handle the pending state on form
```tsx filename="app/ui/signup-form.tsx" highlight={6} switcher
'use client'

import { useFormStatus, useFormState } from 'react-dom'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'

export function SignupButton() {
const { pending } = useFormStatus()
Expand All @@ -309,7 +310,8 @@ export function SignupButton() {
```jsx filename="app/ui/signup-form.js" highlight={6} switcher
'use client'

import { useFormStatus, useFormState } from 'react-dom'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'

export function SignupButton() {
const { pending } = useFormStatus()
Expand Down
5 changes: 3 additions & 2 deletions examples/next-forms/app/add-form.tsx
@@ -1,6 +1,7 @@
"use client";

import { useFormState, useFormStatus } from "react-dom";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { createTodo } from "@/app/actions";

const initialState = {
Expand All @@ -18,7 +19,7 @@ function SubmitButton() {
}

export function AddForm() {
const [state, formAction] = useFormState(createTodo, initialState);
const [state, formAction] = useActionState(createTodo, initialState);

return (
<form action={formAction}>
Expand Down
5 changes: 3 additions & 2 deletions examples/next-forms/app/delete-form.tsx
@@ -1,6 +1,7 @@
"use client";

import { useFormState, useFormStatus } from "react-dom";
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { deleteTodo } from "@/app/actions";

const initialState = {
Expand All @@ -18,7 +19,7 @@ function DeleteButton() {
}

export function DeleteForm({ id, todo }: { id: number; todo: string }) {
const [state, formAction] = useFormState(deleteTodo, initialState);
const [state, formAction] = useActionState(deleteTodo, initialState);

return (
<form action={formAction}>
Expand Down
6 changes: 4 additions & 2 deletions examples/with-fauna/components/EntryForm.tsx
Expand Up @@ -3,7 +3,9 @@
import cn from "classnames";
import { createEntryAction } from "@/actions/entry";
// @ts-ignore
import { useFormState, useFormStatus } from "react-dom";
import { useActionState } from "react";
// @ts-ignore
import { useFormStatus } from "react-dom";
import LoadingSpinner from "@/components/LoadingSpinner";
import SuccessMessage from "@/components/SuccessMessage";
import ErrorMessage from "@/components/ErrorMessage";
Expand All @@ -20,7 +22,7 @@ const initialState = {
};

export default function EntryForm() {
const [state, formAction] = useFormState(createEntryAction, initialState);
const [state, formAction] = useActionState(createEntryAction, initialState);
const { pending } = useFormStatus();

return (
Expand Down
30 changes: 17 additions & 13 deletions package.json
Expand Up @@ -196,18 +196,18 @@
"pretty-bytes": "5.3.0",
"pretty-ms": "7.0.0",
"random-seed": "0.3.0",
"react": "18.2.0",
"react": "19.0.0-beta-4508873393-20240430",
"react-17": "npm:react@17.0.2",
"react-builtin": "npm:react@18.3.0-canary-c3048aab4-20240326",
"react-dom": "18.2.0",
"react-builtin": "npm:react@19.0.0-beta-4508873393-20240430",
"react-dom": "19.0.0-beta-4508873393-20240430",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-builtin": "npm:react-dom@18.3.0-canary-c3048aab4-20240326",
"react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-c3048aab4-20240326",
"react-experimental-builtin": "npm:react@0.0.0-experimental-c3048aab4-20240326",
"react-server-dom-turbopack": "18.3.0-canary-c3048aab4-20240326",
"react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-c3048aab4-20240326",
"react-server-dom-webpack": "18.3.0-canary-c3048aab4-20240326",
"react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-c3048aab4-20240326",
"react-dom-builtin": "npm:react-dom@19.0.0-beta-4508873393-20240430",
"react-dom-experimental-builtin": "npm:react-dom@0.0.0-experimental-4508873393-20240430",
"react-experimental-builtin": "npm:react@0.0.0-experimental-4508873393-20240430",
"react-server-dom-turbopack": "19.0.0-beta-4508873393-20240430",
"react-server-dom-turbopack-experimental": "npm:react-server-dom-turbopack@0.0.0-experimental-4508873393-20240430",
"react-server-dom-webpack": "19.0.0-beta-4508873393-20240430",
"react-server-dom-webpack-experimental": "npm:react-server-dom-webpack@0.0.0-experimental-4508873393-20240430",
"react-ssr-prepass": "1.0.8",
"react-virtualized": "9.22.3",
"relay-compiler": "13.0.2",
Expand All @@ -217,8 +217,8 @@
"resolve-from": "5.0.0",
"sass": "1.54.0",
"satori": "0.10.9",
"scheduler-builtin": "npm:scheduler@0.24.0-canary-c3048aab4-20240326",
"scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-c3048aab4-20240326",
"scheduler-builtin": "npm:scheduler@0.25.0-beta-4508873393-20240430",
"scheduler-experimental-builtin": "npm:scheduler@0.0.0-experimental-4508873393-20240430",
"seedrandom": "3.0.5",
"selenium-webdriver": "4.0.0-beta.4",
"semver": "7.3.7",
Expand Down Expand Up @@ -252,7 +252,11 @@
"@babel/types": "7.22.5",
"@babel/traverse": "7.22.5",
"@types/react": "18.2.74",
"@types/react-dom": "18.2.23"
"@types/react-dom": "18.2.23",
"react": "19.0.0-beta-4508873393-20240430",
"react-dom": "19.0.0-beta-4508873393-20240430",
"react-is": "19.0.0-beta-4508873393-20240430",
"scheduler": "0.25.0-beta-94eed63c49-20240425"
},
"engines": {
"node": ">=18.17.0",
Expand Down
25 changes: 15 additions & 10 deletions packages/next-swc/crates/next-core/src/next_import_map.rs
Expand Up @@ -728,16 +728,21 @@ async fn rsc_aliases(
}
}

if runtime == NextRuntime::Edge {
if ty.supports_react_server() {
alias["react"] = format!("next/dist/compiled/react{react_channel}/react.react-server");
alias["react-dom"] =
format!("next/dist/compiled/react-dom{react_channel}/react-dom.react-server");
} else {
// x-ref: https://github.com/facebook/react/pull/25436
alias["react-dom"] =
format!("next/dist/compiled/react-dom{react_channel}/server-rendering-stub");
}
if runtime == NextRuntime::Edge && ty.supports_react_server() {
alias.extend(indexmap! {
"react" => format!("next/dist/compiled/react{react_channel}/react.react-server"),
"next/dist/compiled/react" => format!("next/dist/compiled/react{react_channel}/react.react-server"),
"next/dist/compiled/react-experimental" => format!("next/dist/compiled/react-experimental/react.react-server"),
"react/jsx-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-runtime.react-server"),
"next/dist/compiled/react/jsx-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-runtime.react-server"),
"next/dist/compiled/react-experimental/jsx-runtime" => format!("next/dist/compiled/react-experimental/jsx-runtime.react-server"),
"react/jsx-dev-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-dev-runtime.react-server"),
"next/dist/compiled/react/jsx-dev-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-dev-runtime.react-server"),
"next/dist/compiled/react-experimental/jsx-dev-runtime" => format!("next/dist/compiled/react-experimental/jsx-dev-runtime.react-server"),
"react-dom" => format!("next/dist/compiled/react-dom{react_channel}/react-dom.react-server"),
"next/dist/compiled/react-dom" => format!("next/dist/compiled/react-dom{react_channel}/react-dom.react-server"),
"next/dist/compiled/react-dom-experimental" => format!("next/dist/compiled/react-dom-experimental/react-dom.react-server"),
})
}

insert_exact_alias_map(import_map, project_path, alias);
Expand Down
Expand Up @@ -506,12 +506,12 @@ impl ReactServerComponentValidator {
"useSyncExternalStore",
"useTransition",
"useOptimistic",
"useActionState",
],
),
(
"react-dom",
vec![
"findDOMNode",
"flushSync",
"unstable_batchedUpdates",
"useFormStatus",
Expand Down
@@ -1,4 +1,6 @@
import { findDOMNode, flushSync, unstable_batchedUpdates } from 'react-dom'
import { flushSync, unstable_batchedUpdates } from 'react-dom'

import { useActionState } from 'react'

import { useFormStatus, useFormState } from 'react-dom'

Expand Down
@@ -1,4 +1,5 @@
import { findDOMNode, flushSync, unstable_batchedUpdates } from 'react-dom';
import { flushSync, unstable_batchedUpdates } from 'react-dom';
import { useActionState } from 'react'
import { useFormStatus, useFormState } from 'react-dom';
export default function() {
return null;
Expand Down
@@ -1,48 +1,49 @@

x You're importing a component that needs findDOMNode. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
x You're importing a component that needs flushSync. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
| Learn more: https://nextjs.org/docs/getting-started/react-essentials
|
|
,-[input.js:1:1]
1 | import { findDOMNode, flushSync, unstable_batchedUpdates } from 'react-dom'
: ^^^^^^^^^^^
1 | import { flushSync, unstable_batchedUpdates } from 'react-dom'
: ^^^^^^^^^
`----

x You're importing a component that needs flushSync. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
x You're importing a component that needs unstable_batchedUpdates. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by
| default.
| Learn more: https://nextjs.org/docs/getting-started/react-essentials
|
|
,-[input.js:1:1]
1 | import { findDOMNode, flushSync, unstable_batchedUpdates } from 'react-dom'
: ^^^^^^^^^
1 | import { flushSync, unstable_batchedUpdates } from 'react-dom'
: ^^^^^^^^^^^^^^^^^^^^^^^
`----

x You're importing a component that needs unstable_batchedUpdates. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by
| default.
x You're importing a component that needs useActionState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
| Learn more: https://nextjs.org/docs/getting-started/react-essentials
|
|
,-[input.js:1:1]
1 | import { findDOMNode, flushSync, unstable_batchedUpdates } from 'react-dom'
: ^^^^^^^^^^^^^^^^^^^^^^^
,-[input.js:2:1]
2 |
3 | import { useActionState } from 'react'
: ^^^^^^^^^^^^^^
`----

x You're importing a component that needs useFormStatus. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
| Learn more: https://nextjs.org/docs/getting-started/react-essentials
|
|
,-[input.js:2:1]
2 |
3 | import { useFormStatus, useFormState } from 'react-dom'
,-[input.js:4:1]
4 |
5 | import { useFormStatus, useFormState } from 'react-dom'
: ^^^^^^^^^^^^^
`----

x You're importing a component that needs useFormState. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
| Learn more: https://nextjs.org/docs/getting-started/react-essentials
|
|
,-[input.js:2:1]
2 |
3 | import { useFormStatus, useFormState } from 'react-dom'
,-[input.js:4:1]
4 |
5 | import { useFormStatus, useFormState } from 'react-dom'
: ^^^^^^^^^^^^
`----
6 changes: 3 additions & 3 deletions packages/next/package.json
Expand Up @@ -104,8 +104,8 @@
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "19.0.0-beta-4508873393-20240430",
"react-dom": "19.0.0-beta-4508873393-20240430",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
Expand Down Expand Up @@ -282,7 +282,7 @@
"punycode": "2.1.1",
"querystring-es3": "0.2.1",
"raw-body": "2.4.1",
"react-is": "18.2.0",
"react-is": "19.0.0-canary-94eed63c49-20240425",
"react-refresh": "0.12.0",
"regenerator-runtime": "0.13.4",
"sass-loader": "12.4.0",
Expand Down

0 comments on commit 2c31c79

Please sign in to comment.