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 authored 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.
Original file line number Diff line number Diff line change
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)
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,12 @@ impl ReactServerComponentValidator {
"useSyncExternalStore",
"useTransition",
"useOptimistic",
"useActionState",
],
),
(
"react-dom",
vec![
"findDOMNode",
"flushSync",
"unstable_batchedUpdates",
"useFormStatus",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 2c31c79

Please sign in to comment.