Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support React 19 in App and Pages router #65058

Merged
merged 36 commits into from May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
618ceee
Start syncing with Beta
eps1lon Apr 30, 2024
84ef617
Update React from c3048aab4 to 4508873393
eps1lon Apr 24, 2024
cf89684
Minified bundles were removed
eps1lon Apr 26, 2024
41a97c3
Use `react-dom` directly instead of server-rendering-stub
eps1lon Apr 26, 2024
6e8445c
Inline tests for integration/react-18
eps1lon Apr 30, 2024
2009625
Alias JSX runtime
eps1lon Apr 26, 2024
37cf4c9
Use React 19 everywhere
eps1lon Apr 25, 2024
7c388cf
Resolve findDOMNode breaking changes
eps1lon Apr 25, 2024
6cd9405
useFormState -> useActionState
eps1lon Apr 25, 2024
603dc09
Again the react vs next/dist/compiled/react shenanigans
eps1lon Apr 27, 2024
0d0aa62
Implement fetch cache
eps1lon Apr 28, 2024
82f1e90
Fix aliases in vendored React
eps1lon Apr 29, 2024
7cb4d22
Fix image tests
eps1lon Apr 29, 2024
dd32f40
Update tests assuming React.cache works on the client
eps1lon Apr 29, 2024
2275c7d
Work around React bugs
eps1lon Apr 29, 2024
d6699ee
Disable StrictMode tests
eps1lon Apr 29, 2024
6ed9c92
Disable tests that already failed experimental React
eps1lon Apr 29, 2024
37338f1
Update test assuming we use Canary
eps1lon Apr 29, 2024
8e17b5e
Stop using legacy root
eps1lon Apr 29, 2024
7a62dc7
Pages Router: Fix error overlay
eps1lon Apr 24, 2024
64aa113
Adjust tests now that `enableFilterEmptyStringAttributesDOM` has landed
eps1lon Apr 30, 2024
5664c85
Fix profiling aliases
eps1lon Apr 30, 2024
92ca39b
Use Float for image preloading in `next/legacy/image`
eps1lon Apr 30, 2024
e7ce7c0
Port create-compiler-aliases to Turbopack
eps1lon Apr 30, 2024
cb1c174
Update compiled
eps1lon May 1, 2024
8ec7f27
Collapse if because that's what Clippy wants
eps1lon May 1, 2024
7659179
custom scripts fail AMP validation now
eps1lon May 2, 2024
d7c9949
Apply react 19 stack and diff (#65276)
huozhi May 2, 2024
30628ff
Restore collapse/expand for hydration diffs
eps1lon May 2, 2024
d453769
Pages router handles hydration errors as runtime errors
eps1lon May 2, 2024
330b523
Stop using next/head for scripts
eps1lon May 3, 2024
12b9753
Ignore tests using react-relay
eps1lon May 3, 2024
2cc6777
Use next/script the way `next/head > script` behaved
eps1lon May 3, 2024
3e4de91
Browser seems to miss hydration some errors now
eps1lon May 3, 2024
635e901
Charset is now floated to the start in React 19
eps1lon May 6, 2024
71ab968
Float breaks loose head reconciler
eps1lon May 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
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",
huozhi marked this conversation as resolved.
Show resolved Hide resolved
"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