mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Add experimental.useExperimentalReact to opt into React's experimental channel (#94861)
Opting into React's experimental channel — which emits `<link rel="expect">` to hold first paint until the streamed shell is coherent, avoiding flicker from partially-streamed HTML — currently requires enabling an unrelated feature like `experimental.taint` as a side effect, which is confusing. This adds `experimental.useExperimentalReact` as a direct opt-in. It feeds the existing `needsExperimentalReact` aggregation and the matching Turbopack `react_channel` switch, selecting the `react@experimental` build the same way `taint`, `transitionIndicator`, and `gestureTransition` do. It's opt-in only: an explicit `false` can't disable the channel when one of those still requires it (the taint APIs only exist in the experimental build), so `assignDefaults` warns on that contradiction. Covered by a webpack e2e test mirroring the existing `taint` channel test. <!-- NEXT_JS_LLM_PR -->
This commit is contained in:
@@ -1269,6 +1269,7 @@ pub struct ExperimentalConfig {
|
||||
swc_trace_profiling: Option<bool>,
|
||||
transition_indicator: Option<bool>,
|
||||
gesture_transition: Option<bool>,
|
||||
use_experimental_react: Option<bool>,
|
||||
/// @internal Used by the Next.js internals only.
|
||||
trust_host_header: Option<bool>,
|
||||
|
||||
@@ -2223,6 +2224,11 @@ impl NextConfig {
|
||||
Vc::cell(self.experimental.gesture_transition.unwrap_or(false))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub fn enable_use_experimental_react(&self) -> Vc<bool> {
|
||||
Vc::cell(self.experimental.use_experimental_react.unwrap_or(false))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub fn enable_cache_components(&self) -> Vc<bool> {
|
||||
Vc::cell(self.cache_components.unwrap_or(false))
|
||||
|
||||
@@ -86,14 +86,16 @@ pub async fn get_next_client_import_map(
|
||||
}
|
||||
ClientContextType::App { app_dir } => {
|
||||
// Keep in sync with file:///./../../../packages/next/src/lib/needs-experimental-react.ts
|
||||
let use_experimental_react = *next_config.enable_use_experimental_react().await?;
|
||||
let taint = *next_config.enable_taint().await?;
|
||||
let transition_indicator = *next_config.enable_transition_indicator().await?;
|
||||
let gesture_transition = *next_config.enable_gesture_transition().await?;
|
||||
let react_channel = if taint || transition_indicator || gesture_transition {
|
||||
"-experimental"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let react_channel =
|
||||
if use_experimental_react || taint || transition_indicator || gesture_transition {
|
||||
"-experimental"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
import_map.insert_exact_alias(
|
||||
rcstr!("react"),
|
||||
@@ -782,14 +784,16 @@ async fn apply_vendored_react_aliases_server(
|
||||
runtime: NextRuntime,
|
||||
next_config: Vc<NextConfig>,
|
||||
) -> Result<()> {
|
||||
let use_experimental_react = *next_config.enable_use_experimental_react().await?;
|
||||
let taint = *next_config.enable_taint().await?;
|
||||
let transition_indicator = *next_config.enable_transition_indicator().await?;
|
||||
let gesture_transition = *next_config.enable_gesture_transition().await?;
|
||||
let react_channel = if taint || transition_indicator || gesture_transition {
|
||||
"-experimental"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let react_channel =
|
||||
if use_experimental_react || taint || transition_indicator || gesture_transition {
|
||||
"-experimental"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let react_condition = if ty.should_use_react_server_condition() {
|
||||
"server"
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { NextConfig } from '../server/config-shared'
|
||||
|
||||
// Keep in sync with Turbopack's experimental React switch: file://./../../../../crates/next-core/src/next_import_map.rs
|
||||
export function needsExperimentalReact(config: NextConfig) {
|
||||
const { taint, transitionIndicator, gestureTransition } =
|
||||
config.experimental || {}
|
||||
return Boolean(taint || transitionIndicator || gestureTransition)
|
||||
const {
|
||||
useExperimentalReact,
|
||||
taint,
|
||||
transitionIndicator,
|
||||
gestureTransition,
|
||||
} = config.experimental || {}
|
||||
return Boolean(
|
||||
useExperimentalReact || taint || transitionIndicator || gestureTransition
|
||||
)
|
||||
}
|
||||
|
||||
@@ -299,6 +299,7 @@ export const experimentalSchema = {
|
||||
.readonly()
|
||||
.optional(),
|
||||
taint: z.boolean().optional(),
|
||||
useExperimentalReact: z.boolean().optional(),
|
||||
prerenderEarlyExit: z.boolean().optional(),
|
||||
proxyTimeout: z.number().gte(0).optional(),
|
||||
rootParams: z.boolean().optional(),
|
||||
|
||||
@@ -974,6 +974,22 @@ export interface ExperimentalConfig {
|
||||
*/
|
||||
taint?: boolean
|
||||
|
||||
/**
|
||||
* Opt into React's experimental release channel for the `app` directory
|
||||
* without enabling a specific experimental API (such as `taint`).
|
||||
*
|
||||
* The experimental React build emits a `<link rel="expect">` tag that holds
|
||||
* the browser's first paint until the streamed shell is coherent, avoiding
|
||||
* the layout shift / flicker that can occur while a partially-streamed HTML
|
||||
* document is painted. Note that `rel="expect"` is currently only
|
||||
* implemented by Chromium-based browsers.
|
||||
*
|
||||
* This is an opt-in only. Setting it to `false` does not disable the
|
||||
* experimental channel when another feature (such as `taint`,
|
||||
* `transitionIndicator`, or `gestureTransition`) requires it.
|
||||
*/
|
||||
useExperimentalReact?: boolean
|
||||
|
||||
/**
|
||||
* Uninstalls all "unhandledRejection" and "uncaughtException" listeners from
|
||||
* the global process so that we can override the behavior, which in some
|
||||
|
||||
@@ -546,6 +546,30 @@ function assignDefaultsAndValidate(
|
||||
)
|
||||
}
|
||||
|
||||
// `useExperimentalReact` only opts *into* React's experimental channel; it
|
||||
// cannot opt out of it when another feature (e.g. `taint`) structurally
|
||||
// requires it — those APIs only exist in the experimental React build. Warn
|
||||
// so an explicit `false` here doesn't look like it was silently ignored.
|
||||
if (result.experimental.useExperimentalReact === false) {
|
||||
const dependents: string[] = []
|
||||
if (result.experimental.taint) {
|
||||
dependents.push('`experimental.taint`')
|
||||
}
|
||||
if (result.experimental.transitionIndicator) {
|
||||
dependents.push('`experimental.transitionIndicator`')
|
||||
}
|
||||
if (result.experimental.gestureTransition) {
|
||||
dependents.push('`experimental.gestureTransition`')
|
||||
}
|
||||
if (dependents.length > 0) {
|
||||
Log.warn(
|
||||
`\`experimental.useExperimentalReact\` is set to \`false\`, but ${dependents.join(
|
||||
', '
|
||||
)} require React's experimental channel, so it remains enabled.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (result.experimental.appShells) {
|
||||
// App Shells is tested in combination with the experimental flags it
|
||||
// expects to ship alongside. All of these are on track to become
|
||||
|
||||
@@ -247,6 +247,7 @@
|
||||
"test/e2e/app-dir/root-layout/root-layout.test.ts",
|
||||
"test/e2e/app-dir/router-stuck-dynamic-static-segment/router-stuck-dynamic-static-segment.test.ts",
|
||||
"test/e2e/app-dir/rsc-basic/rsc-basic-react-experimental.test.ts",
|
||||
"test/e2e/app-dir/rsc-basic/rsc-basic-use-experimental-react.test.ts",
|
||||
"test/e2e/app-dir/rsc-basic/rsc-basic.test.ts",
|
||||
"test/e2e/app-dir/rsc-redirect/rsc-redirect.test.ts",
|
||||
"test/e2e/app-dir/scss/hmr-module/hmr-module.test.ts",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { nextTestSetup } from 'e2e-utils'
|
||||
|
||||
describe('react@experimental - useExperimentalReact', () => {
|
||||
const { next } = nextTestSetup({
|
||||
files: __dirname,
|
||||
overrideFiles: {
|
||||
'next.config.js': `
|
||||
module.exports = {
|
||||
experimental: {
|
||||
useExperimentalReact: true,
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
})
|
||||
|
||||
it('should opt into the react@experimental channel when useExperimentalReact is enabled', async () => {
|
||||
const resPages$ = await next.render$('/app-react')
|
||||
const [
|
||||
ssrReact,
|
||||
ssrReactDOM,
|
||||
ssrClientReact,
|
||||
ssrClientReactDOM,
|
||||
ssrClientReactDOMServer,
|
||||
] = [
|
||||
resPages$('#react').text(),
|
||||
resPages$('#react-dom').text(),
|
||||
resPages$('#client-react').text(),
|
||||
resPages$('#client-react-dom').text(),
|
||||
resPages$('#client-react-dom-server').text(),
|
||||
]
|
||||
expect({
|
||||
ssrReact,
|
||||
ssrReactDOM,
|
||||
ssrClientReact,
|
||||
ssrClientReactDOM,
|
||||
ssrClientReactDOMServer,
|
||||
}).toEqual({
|
||||
ssrReact: expect.stringMatching('-experimental-'),
|
||||
ssrReactDOM: expect.stringMatching('-experimental-'),
|
||||
ssrClientReact: expect.stringMatching('-experimental-'),
|
||||
ssrClientReactDOM: expect.stringMatching('-experimental-'),
|
||||
ssrClientReactDOMServer: expect.stringMatching('-experimental-'),
|
||||
})
|
||||
|
||||
const browser = await next.browser('/app-react')
|
||||
const [
|
||||
browserReact,
|
||||
browserReactDOM,
|
||||
browserClientReact,
|
||||
browserClientReactDOM,
|
||||
browserClientReactDOMServer,
|
||||
] = await browser.eval(`
|
||||
[
|
||||
document.querySelector('#react').innerText,
|
||||
document.querySelector('#react-dom').innerText,
|
||||
document.querySelector('#client-react').innerText,
|
||||
document.querySelector('#client-react-dom').innerText,
|
||||
document.querySelector('#client-react-dom-server').innerText,
|
||||
]
|
||||
`)
|
||||
expect({
|
||||
browserReact,
|
||||
browserReactDOM,
|
||||
browserClientReact,
|
||||
browserClientReactDOM,
|
||||
browserClientReactDOMServer,
|
||||
}).toEqual({
|
||||
browserReact: expect.stringMatching('-experimental-'),
|
||||
browserReactDOM: expect.stringMatching('-experimental-'),
|
||||
browserClientReact: expect.stringMatching('-experimental-'),
|
||||
browserClientReactDOM: expect.stringMatching('-experimental-'),
|
||||
browserClientReactDOMServer: expect.stringMatching('-experimental-'),
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user