diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
index 387dafb6e5a1f..19c320b05dfcf 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
@@ -29,6 +29,8 @@ import {
isArrayType,
isMutableEffect,
isObjectType,
+ isRefValueType,
+ isUseRefType,
} from "../HIR/HIR";
import { FunctionSignature } from "../HIR/ObjectShape";
import {
@@ -521,7 +523,12 @@ class InferenceState {
break;
}
case Effect.Mutate: {
- if (valueKind.kind === ValueKind.Context) {
+ if (
+ isRefValueType(place.identifier) ||
+ isUseRefType(place.identifier)
+ ) {
+ // no-op: refs are validate via ValidateNoRefAccessInRender
+ } else if (valueKind.kind === ValueKind.Context) {
functionEffect = {
kind: "ContextMutation",
loc: place.loc,
@@ -560,7 +567,12 @@ class InferenceState {
break;
}
case Effect.Store: {
- if (valueKind.kind === ValueKind.Context) {
+ if (
+ isRefValueType(place.identifier) ||
+ isUseRefType(place.identifier)
+ ) {
+ // no-op: refs are validate via ValidateNoRefAccessInRender
+ } else if (valueKind.kind === ValueKind.Context) {
functionEffect = {
kind: "ContextMutation",
loc: place.loc,
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md
new file mode 100644
index 0000000000000..539c9e71ec828
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md
@@ -0,0 +1,109 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(10);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const setRef = t0;
+ let t1;
+ if ($[1] !== setRef) {
+ t1 = () => {
+ setRef();
+ };
+ $[1] = setRef;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ const onClick = t1;
+ let t2;
+ if ($[3] !== ref) {
+ t2 = ;
+ $[3] = ref;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== onClick) {
+ t3 = ;
+ $[5] = onClick;
+ $[6] = t3;
+ } else {
+ t3 = $[6];
+ }
+ let t4;
+ if ($[7] !== t2 || $[8] !== t3) {
+ t4 = (
+ <>
+ {t2}
+ {t3}
+ >
+ );
+ $[7] = t2;
+ $[8] = t3;
+ $[9] = t4;
+ } else {
+ t4 = $[9];
+ }
+ return t4;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx
new file mode 100644
index 0000000000000..14a5fd9aa5bc4
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx
@@ -0,0 +1,28 @@
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md
new file mode 100644
index 0000000000000..5a70f6c86eeca
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md
@@ -0,0 +1,94 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(8);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const onClick = t0;
+ let t1;
+ if ($[1] !== ref) {
+ t1 = ;
+ $[1] = ref;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ let t2;
+ if ($[3] !== onClick) {
+ t2 = ;
+ $[3] = onClick;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== t1 || $[6] !== t2) {
+ t3 = (
+ <>
+ {t1}
+ {t2}
+ >
+ );
+ $[5] = t1;
+ $[6] = t2;
+ $[7] = t3;
+ } else {
+ t3 = $[7];
+ }
+ return t3;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx
new file mode 100644
index 0000000000000..74410e119a285
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx
@@ -0,0 +1,24 @@
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md
new file mode 100644
index 0000000000000..088ad294ef7b8
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md
@@ -0,0 +1,109 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(10);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const setRef = t0;
+ let t1;
+ if ($[1] !== setRef) {
+ t1 = () => {
+ setRef();
+ };
+ $[1] = setRef;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ const onClick = t1;
+ let t2;
+ if ($[3] !== ref) {
+ t2 = ;
+ $[3] = ref;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== onClick) {
+ t3 = ;
+ $[5] = onClick;
+ $[6] = t3;
+ } else {
+ t3 = $[6];
+ }
+ let t4;
+ if ($[7] !== t2 || $[8] !== t3) {
+ t4 = (
+ <>
+ {t2}
+ {t3}
+ >
+ );
+ $[7] = t2;
+ $[8] = t3;
+ $[9] = t4;
+ } else {
+ t4 = $[9];
+ }
+ return t4;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx
new file mode 100644
index 0000000000000..87cc6d42446be
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx
@@ -0,0 +1,28 @@
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md
new file mode 100644
index 0000000000000..f0c115952fa6e
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md
@@ -0,0 +1,94 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(8);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const onClick = t0;
+ let t1;
+ if ($[1] !== ref) {
+ t1 = ;
+ $[1] = ref;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ let t2;
+ if ($[3] !== onClick) {
+ t2 = ;
+ $[3] = onClick;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== t1 || $[6] !== t2) {
+ t3 = (
+ <>
+ {t1}
+ {t2}
+ >
+ );
+ $[5] = t1;
+ $[6] = t2;
+ $[7] = t3;
+ } else {
+ t3 = $[7];
+ }
+ return t3;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx
new file mode 100644
index 0000000000000..b9b7a2dd8e67b
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx
@@ -0,0 +1,24 @@
+// @validateRefAccessDuringRender
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md
new file mode 100644
index 0000000000000..b5547a1328629
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md
@@ -0,0 +1,28 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+function Component() {
+ const ref = useRef(null);
+ ref.current = false;
+
+ return ;
+}
+
+```
+
+
+## Error
+
+```
+ 2 | function Component() {
+ 3 | const ref = useRef(null);
+> 4 | ref.current = false;
+ | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
+ 5 |
+ 6 | return ;
+ 7 | }
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.js
new file mode 100644
index 0000000000000..714eb2d32065d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.js
@@ -0,0 +1,7 @@
+// @validateRefAccessDuringRender
+function Component() {
+ const ref = useRef(null);
+ ref.current = false;
+
+ return ;
+}
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md
new file mode 100644
index 0000000000000..0d4cb1318911b
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md
@@ -0,0 +1,35 @@
+
+## Input
+
+```javascript
+// @validateRefAccessDuringRender
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ ref.current = false;
+ };
+ const changeRef = setRef;
+ changeRef();
+
+ return ;
+}
+
+```
+
+
+## Error
+
+```
+ 7 | };
+ 8 | const changeRef = setRef;
+> 9 | changeRef();
+ | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $39[11:13]:TObject accesses a ref (9:9)
+
+InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
+ 10 |
+ 11 | return ;
+ 12 | }
+```
+
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.js
new file mode 100644
index 0000000000000..93857a55645a5
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.js
@@ -0,0 +1,12 @@
+// @validateRefAccessDuringRender
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ ref.current = false;
+ };
+ const changeRef = setRef;
+ changeRef();
+
+ return ;
+}
diff --git a/compiler/packages/snap/src/runner-watch.ts b/compiler/packages/snap/src/runner-watch.ts
index 85e460ae802cf..414d99084c52e 100644
--- a/compiler/packages/snap/src/runner-watch.ts
+++ b/compiler/packages/snap/src/runner-watch.ts
@@ -153,8 +153,8 @@ function subscribeFilterFile(
} else if (
events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !== -1
) {
- state.filter = await readTestFilter();
if (state.mode.filter) {
+ state.filter = await readTestFilter();
state.mode.action = RunnerAction.Test;
onChange(state);
}
@@ -219,7 +219,7 @@ export async function makeWatchRunner(
action: RunnerAction.Test,
filter: filterMode,
},
- filter: await readTestFilter(),
+ filter: filterMode ? await readTestFilter() : null,
};
subscribeTsc(state, onChange);
diff --git a/compiler/scripts/release/publish-manual.js b/compiler/scripts/release/publish-manual.js
index 203510983ec43..9b5e4fa24459a 100644
--- a/compiler/scripts/release/publish-manual.js
+++ b/compiler/scripts/release/publish-manual.js
@@ -51,8 +51,6 @@ async function getDateStringForCommit(commit) {
}
/**
- * Please login to npm first with `npm login`. You will also need 2FA enabled to push to npm.
- *
* Script for publishing PUBLISHABLE_PACKAGES to npm. By default, this runs in tarball mode, meaning
* the script will only print out what the contents of the files included in the npm tarball would
* be.
@@ -60,10 +58,11 @@ async function getDateStringForCommit(commit) {
* Please run this first (ie `yarn npm:publish`) and double check the contents of the files that
* will be pushed to npm.
*
- * If it looks good, you can run `yarn npm:publish --for-real` to really publish to npm. There's a
- * small annoying delay before the packages are actually pushed to give you time to panic cancel. In
- * this mode, we will bump the version field of each package's package.json, and git commit it.
- * Then, the packages will be published to npm.
+ * If it looks good, you can run `yarn npm:publish --for-real` to really publish to npm. You must
+ * have 2FA enabled first and the script will prompt you to enter a 2FA code before proceeding.
+ * There's a small annoying delay before the packages are actually pushed to give you time to panic
+ * cancel. In this mode, we will bump the version field of each package's package.json, and git
+ * commit it. Then, the packages will be published to npm.
*
* Optionally, you can add the `--debug` flag to `yarn npm:publish --debug --for-real` to run all
* steps, but the final npm publish step will have the `--dry-run` flag added to it. This will make
diff --git a/fixtures/flight/config/webpack.config.js b/fixtures/flight/config/webpack.config.js
index 665cd37216d6d..69be1859fdc6c 100644
--- a/fixtures/flight/config/webpack.config.js
+++ b/fixtures/flight/config/webpack.config.js
@@ -7,6 +7,7 @@ const ReactFlightWebpackPlugin = require('react-server-dom-webpack/plugin');
const fs = require('fs');
const {createHash} = require('crypto');
const path = require('path');
+const {pathToFileURL} = require('url');
const webpack = require('webpack');
const resolve = require('resolve');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
@@ -235,7 +236,7 @@ module.exports = function (webpackEnv) {
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
- (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
+ (info => pathToFileURL(path.resolve(info.absoluteResourcePath))),
},
cache: {
type: 'filesystem',
diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js
index 4313f48502da1..bc4ba05ddf3b4 100644
--- a/fixtures/flight/server/region.js
+++ b/fixtures/flight/server/region.js
@@ -3,6 +3,7 @@
// This is a server to host data-local resources like databases and RSC
const path = require('path');
+const url = require('url');
const register = require('react-server-dom-webpack/node-register');
register();
@@ -192,7 +193,7 @@ if (process.env.NODE_ENV === 'development') {
// We assume that if it was prefixed with file:// it's referring to the compiled output
// and if it's a direct file path we assume it's source mapped back to original format.
isCompiledOutput = true;
- requestedFilePath = requestedFilePath.slice(7);
+ requestedFilePath = url.fileURLToPath(requestedFilePath);
}
const relativePath = path.relative(rootDir, requestedFilePath);
@@ -206,24 +207,41 @@ if (process.env.NODE_ENV === 'development') {
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
let map;
- // There are two ways to return a source map depending on what we observe in error.stack.
- // A real app will have a similar choice to make for which strategy to pick.
- if (!sourceMap || !isCompiledOutput) {
+ if (requestedFilePath.startsWith('node:')) {
+ // This is a node internal. We don't include any source code for this but we still
+ // generate a source map for it so that we can add it to an ignoreList automatically.
+ map = {
+ version: 3,
+ // We use the node:// protocol convention to teach Chrome DevTools that this is
+ // on a different protocol and not part of the current page.
+ sources: ['node:///' + requestedFilePath.slice(5)],
+ sourcesContent: ['// Node Internals'],
+ mappings: 'AAAA',
+ ignoreList: [0],
+ sourceRoot: '',
+ };
+ } else if (!sourceMap || !isCompiledOutput) {
// If a file doesn't have a source map, such as this file, then we generate a blank
// source map that just contains the original content and segments pointing to the
- // original lines.
- // Similarly
+ // original lines. If a line number points to uncompiled output, like if source mapping
+ // was already applied we also use this path.
const sourceContent = await readFile(requestedFilePath, 'utf8');
const lines = sourceContent.split('\n').length;
+ // We ensure to absolute
+ const sourceURL = url.pathToFileURL(requestedFilePath);
map = {
version: 3,
- sources: [requestedFilePath],
+ sources: [sourceURL],
sourcesContent: [sourceContent],
// Note: This approach to mapping each line only lets you jump to each line
// not jump to a column within a line. To do that, you need a proper source map
// generated for each parsed segment or add a segment for each column.
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
sourceRoot: '',
+ // Add any node_modules to the ignore list automatically.
+ ignoreList: requestedFilePath.includes('node_modules')
+ ? [0]
+ : undefined,
};
} else {
// We always set prepareStackTrace before reading the stack so that we get the stack
diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js
index f5b3e7406b348..b4b538a3a9992 100644
--- a/fixtures/flight/src/index.js
+++ b/fixtures/flight/src/index.js
@@ -40,7 +40,11 @@ async function hydrateApp() {
{
callServer,
findSourceMapURL(fileName) {
- return '/source-maps?name=' + encodeURIComponent(fileName);
+ return (
+ document.location.origin +
+ '/source-maps?name=' +
+ encodeURIComponent(fileName)
+ );
},
}
);
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index a66c59dc21434..05ef3b352b5d3 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -1586,12 +1586,36 @@ function resolveErrorDev(
'resolveErrorDev should never be called in production mode. Use resolveErrorProd instead. This is a bug in React.',
);
}
- // eslint-disable-next-line react-internal/prod-error-codes
- const error = new Error(
- message ||
- 'An error occurred in the Server Components render but no message was provided',
- );
- error.stack = stack;
+
+ let error;
+ if (!enableOwnerStacks) {
+ // Executing Error within a native stack isn't really limited to owner stacks
+ // but we gate it behind the same flag for now while iterating.
+ // eslint-disable-next-line react-internal/prod-error-codes
+ error = Error(
+ message ||
+ 'An error occurred in the Server Components render but no message was provided',
+ );
+ error.stack = stack;
+ } else {
+ const callStack = buildFakeCallStack(
+ response,
+ stack,
+ // $FlowFixMe[incompatible-use]
+ Error.bind(
+ null,
+ message ||
+ 'An error occurred in the Server Components render but no message was provided',
+ ),
+ );
+ const rootTask = response._debugRootTask;
+ if (rootTask != null) {
+ error = rootTask.run(callStack);
+ } else {
+ error = callStack();
+ }
+ }
+
(error: any).digest = digest;
const errorWithDigest: ErrorWithDigest = (error: any);
const chunks = response._chunks;
@@ -1677,6 +1701,7 @@ const fakeFunctionCache: Map> = __DEV__
? new Map()
: (null: any);
+let fakeFunctionIdx = 0;
function createFakeFunction(
name: string,
filename: string,
@@ -1695,20 +1720,36 @@ function createFakeFunction(
// point to the original source.
let code;
if (line <= 1) {
- code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n';
+ code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment;
} else {
code =
comment +
'\n'.repeat(line - 2) +
'_=>\n' +
' '.repeat(col < 1 ? 0 : col - 1) +
- '_()\n';
+ '_()';
+ }
+
+ if (filename.startsWith('/')) {
+ // If the filename starts with `/` we assume that it is a file system file
+ // rather than relative to the current host. Since on the server fully qualified
+ // stack traces use the file path.
+ // TODO: What does this look like on Windows?
+ filename = 'file://' + filename;
}
if (sourceMap) {
- code += '//# sourceMappingURL=' + sourceMap;
+ // We use the prefix rsc://React/ to separate these from other files listed in
+ // the Chrome DevTools. We need a "host name" and not just a protocol because
+ // otherwise the group name becomes the root folder. Ideally we don't want to
+ // show these at all but there's two reasons to assign a fake URL.
+ // 1) A printed stack trace string needs a unique URL to be able to source map it.
+ // 2) If source maps are disabled or fails, you should at least be able to tell
+ // which file it was.
+ code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
+ code += '\n//# sourceMappingURL=' + sourceMap;
} else if (filename) {
- code += '//# sourceURL=' + filename;
+ code += '\n//# sourceURL=' + filename;
}
let fn: FakeFunction;
diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js
index c6ce366df04b2..6e065cdc93906 100644
--- a/packages/react-devtools-shared/src/__tests__/store-test.js
+++ b/packages/react-devtools-shared/src/__tests__/store-test.js
@@ -1916,11 +1916,9 @@ describe('Store', () => {
});
// In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088
- // When the error is emitted, the source fiber of this error is not yet mounted
- // So DevTools can't connect the error and the fiber
- // TODO(hoxyq): update RDT to keep track of such fibers
- // @reactVersion >= 19.0
- it('from react get counted [React >= 19]', () => {
+ // The warning is moved to the Child instead of the Parent.
+ // @reactVersion >= 19.0.1
+ it('from react get counted [React >= 19.0.1]', () => {
function Example() {
return [];
}
@@ -1936,9 +1934,10 @@ describe('Store', () => {
);
expect(store).toMatchInlineSnapshot(`
+ ✕ 1, ⚠ 0
[root]
▾
-
+ ✕
`);
});
diff --git a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js
index 425f4abd13346..1c1475534f3e0 100644
--- a/packages/react-dom/src/__tests__/ReactChildReconciler-test.js
+++ b/packages/react-dom/src/__tests__/ReactChildReconciler-test.js
@@ -129,6 +129,7 @@ describe('ReactChildReconciler', () => {
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
' in div (at **)\n' +
+ (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
' in Component (at **)\n' +
(gate(flags => flags.enableOwnerStacks)
? ''
@@ -190,6 +191,7 @@ describe('ReactChildReconciler', () => {
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
' in div (at **)\n' +
+ (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
' in Component (at **)\n' +
(gate(flags => flags.enableOwnerStacks)
? ''
diff --git a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
index 4bea59a7b8e53..3108793c87dcf 100644
--- a/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
+++ b/packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
@@ -373,7 +373,7 @@ describe('ReactComponentLifeCycle', () => {
expect(instance.updater.isMounted(instance)).toBe(false);
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic
it('warns if legacy findDOMNode is used inside render', async () => {
class Component extends React.Component {
state = {isMounted: false};
diff --git a/packages/react-dom/src/__tests__/ReactMultiChild-test.js b/packages/react-dom/src/__tests__/ReactMultiChild-test.js
index 1ffc793f96023..087e92985057b 100644
--- a/packages/react-dom/src/__tests__/ReactMultiChild-test.js
+++ b/packages/react-dom/src/__tests__/ReactMultiChild-test.js
@@ -227,12 +227,13 @@ describe('ReactMultiChild', () => {
'across updates. Non-unique keys may cause children to be ' +
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
- ' in div (at **)\n' +
- ' in WrapperComponent (at **)\n' +
+ ' in div (at **)' +
(gate(flags => flags.enableOwnerStacks)
? ''
- : ' in div (at **)\n') +
- ' in Parent (at **)',
+ : '\n in div (at **)' +
+ '\n in WrapperComponent (at **)' +
+ '\n in div (at **)' +
+ '\n in Parent (at **)'),
);
});
@@ -292,12 +293,13 @@ describe('ReactMultiChild', () => {
'across updates. Non-unique keys may cause children to be ' +
'duplicated and/or omitted — the behavior is unsupported and ' +
'could change in a future version.\n' +
- ' in div (at **)\n' +
- ' in WrapperComponent (at **)\n' +
+ ' in div (at **)' +
(gate(flags => flags.enableOwnerStacks)
? ''
- : ' in div (at **)\n') +
- ' in Parent (at **)',
+ : '\n in div (at **)' +
+ '\n in WrapperComponent (at **)' +
+ '\n in div (at **)' +
+ '\n in Parent (at **)'),
);
});
});
diff --git a/packages/react-dom/src/__tests__/findDOMNodeFB-test.js b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js
index 76cb53beba5ec..850ba8f1815b1 100644
--- a/packages/react-dom/src/__tests__/findDOMNodeFB-test.js
+++ b/packages/react-dom/src/__tests__/findDOMNodeFB-test.js
@@ -14,12 +14,12 @@ const ReactDOM = require('react-dom');
const StrictMode = React.StrictMode;
describe('findDOMNode', () => {
- // @gate www && !disableLegacyMode
+ // @gate www && classic
it('findDOMNode should return null if passed null', () => {
expect(ReactDOM.findDOMNode(null)).toBe(null);
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should find dom element', () => {
class MyNode extends React.Component {
render() {
@@ -39,7 +39,7 @@ describe('findDOMNode', () => {
expect(mySameDiv).toBe(myDiv);
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should find dom element after an update from null', () => {
function Bar({flag}) {
if (flag) {
@@ -66,14 +66,14 @@ describe('findDOMNode', () => {
expect(b.tagName).toBe('SPAN');
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic
it('findDOMNode should reject random objects', () => {
expect(function () {
ReactDOM.findDOMNode({foo: 'bar'});
}).toThrowError('Argument appears to not be a ReactComponent. Keys: foo');
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should reject unmounted objects with render func', () => {
class Foo extends React.Component {
render() {
@@ -90,7 +90,7 @@ describe('findDOMNode', () => {
);
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should not throw an error when called within a component that is not mounted', () => {
class Bar extends React.Component {
UNSAFE_componentWillMount() {
@@ -107,7 +107,7 @@ describe('findDOMNode', () => {
}).not.toThrow();
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should warn if used to find a host component inside StrictMode', () => {
let parent = undefined;
let child = undefined;
@@ -141,7 +141,7 @@ describe('findDOMNode', () => {
expect(match).toBe(child);
});
- // @gate www && !disableLegacyMode
+ // @gate www && classic && !disableLegacyMode
it('findDOMNode should warn if passed a component that is inside StrictMode', () => {
let parent = undefined;
let child = undefined;
diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js
index 0220f988c15db..9334107301b0c 100644
--- a/packages/react-reconciler/src/ReactChildFiber.js
+++ b/packages/react-reconciler/src/ReactChildFiber.js
@@ -93,7 +93,11 @@ let didWarnAboutGenerators;
let ownerHasKeyUseWarning;
let ownerHasFunctionTypeWarning;
let ownerHasSymbolTypeWarning;
-let warnForMissingKey = (child: mixed, returnFiber: Fiber) => {};
+let warnForMissingKey = (
+ returnFiber: Fiber,
+ workInProgress: Fiber,
+ child: mixed,
+) => {};
if (__DEV__) {
didWarnAboutMaps = false;
@@ -108,7 +112,11 @@ if (__DEV__) {
ownerHasFunctionTypeWarning = ({}: {[string]: boolean});
ownerHasSymbolTypeWarning = ({}: {[string]: boolean});
- warnForMissingKey = (child: mixed, returnFiber: Fiber) => {
+ warnForMissingKey = (
+ returnFiber: Fiber,
+ workInProgress: Fiber,
+ child: mixed,
+ ) => {
if (child === null || typeof child !== 'object') {
return;
}
@@ -172,14 +180,7 @@ if (__DEV__) {
}
}
- // We create a fake Fiber for the child to log the stack trace from.
- // TODO: Refactor the warnForMissingKey calls to happen after fiber creation
- // so that we can get access to the fiber that will eventually be created.
- // That way the log can show up associated with the right instance in DevTools.
- const fiber = createFiberFromElement((child: any), returnFiber.mode, 0);
- fiber.return = returnFiber;
-
- runWithFiberInDEV(fiber, () => {
+ runWithFiberInDEV(workInProgress, () => {
console.error(
'Each child in a list should have a unique "key" prop.' +
'%s%s See https://react.dev/link/warning-keys for more information.',
@@ -1034,9 +1035,10 @@ function createChildReconciler(
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey(
+ returnFiber: Fiber,
+ workInProgress: Fiber,
child: mixed,
knownKeys: Set | null,
- returnFiber: Fiber,
): Set | null {
if (__DEV__) {
if (typeof child !== 'object' || child === null) {
@@ -1045,7 +1047,7 @@ function createChildReconciler(
switch (child.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
- warnForMissingKey(child, returnFiber);
+ warnForMissingKey(returnFiber, workInProgress, child);
const key = child.key;
if (typeof key !== 'string') {
break;
@@ -1059,14 +1061,16 @@ function createChildReconciler(
knownKeys.add(key);
break;
}
- console.error(
- 'Encountered two children with the same key, `%s`. ' +
- 'Keys should be unique so that components maintain their identity ' +
- 'across updates. Non-unique keys may cause children to be ' +
- 'duplicated and/or omitted — the behavior is unsupported and ' +
- 'could change in a future version.',
- key,
- );
+ runWithFiberInDEV(workInProgress, () => {
+ console.error(
+ 'Encountered two children with the same key, `%s`. ' +
+ 'Keys should be unique so that components maintain their identity ' +
+ 'across updates. Non-unique keys may cause children to be ' +
+ 'duplicated and/or omitted — the behavior is unsupported and ' +
+ 'could change in a future version.',
+ key,
+ );
+ });
break;
case REACT_LAZY_TYPE: {
let resolvedChild;
@@ -1077,7 +1081,12 @@ function createChildReconciler(
const init = (child._init: any);
resolvedChild = init(payload);
}
- warnOnInvalidKey(resolvedChild, knownKeys, returnFiber);
+ warnOnInvalidKey(
+ returnFiber,
+ workInProgress,
+ resolvedChild,
+ knownKeys,
+ );
break;
}
default:
@@ -1113,14 +1122,7 @@ function createChildReconciler(
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
- if (__DEV__) {
- // First, validate keys.
- let knownKeys: Set | null = null;
- for (let i = 0; i < newChildren.length; i++) {
- const child = newChildren[i];
- knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
- }
- }
+ let knownKeys: Set | null = null;
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
@@ -1153,6 +1155,16 @@ function createChildReconciler(
}
break;
}
+
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ newChildren[newIdx],
+ knownKeys,
+ );
+ }
+
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1198,6 +1210,14 @@ function createChildReconciler(
if (newFiber === null) {
continue;
}
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ newChildren[newIdx],
+ knownKeys,
+ );
+ }
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
@@ -1228,6 +1248,14 @@ function createChildReconciler(
debugInfo,
);
if (newFiber !== null) {
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ newChildren[newIdx],
+ knownKeys,
+ );
+ }
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
@@ -1410,17 +1438,10 @@ function createChildReconciler(
let knownKeys: Set | null = null;
let step = newChildren.next();
- if (__DEV__) {
- knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber);
- }
for (
;
oldFiber !== null && !step.done;
- newIdx++,
- step = newChildren.next(),
- knownKeys = __DEV__
- ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
- : null
+ newIdx++, step = newChildren.next()
) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
@@ -1445,6 +1466,16 @@ function createChildReconciler(
}
break;
}
+
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ step.value,
+ knownKeys,
+ );
+ }
+
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
// We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1480,19 +1511,19 @@ function createChildReconciler(
if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
- for (
- ;
- !step.done;
- newIdx++,
- step = newChildren.next(),
- knownKeys = __DEV__
- ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
- : null
- ) {
+ for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = createChild(returnFiber, step.value, lanes, debugInfo);
if (newFiber === null) {
continue;
}
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ step.value,
+ knownKeys,
+ );
+ }
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
@@ -1513,15 +1544,7 @@ function createChildReconciler(
const existingChildren = mapRemainingChildren(oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
- for (
- ;
- !step.done;
- newIdx++,
- step = newChildren.next(),
- knownKeys = __DEV__
- ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
- : null
- ) {
+ for (; !step.done; newIdx++, step = newChildren.next()) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
@@ -1531,6 +1554,14 @@ function createChildReconciler(
debugInfo,
);
if (newFiber !== null) {
+ if (__DEV__) {
+ knownKeys = warnOnInvalidKey(
+ returnFiber,
+ newFiber,
+ step.value,
+ knownKeys,
+ );
+ }
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
index 7ac4ead790f60..1eec2ef8656e3 100644
--- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js
+++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js
@@ -322,9 +322,7 @@ describe('ReactJSXElementValidator', () => {
>,
);
});
- }).toErrorDev('Encountered two children with the same key, `a`.', {
- withoutStack: true,
- });
+ }).toErrorDev('Encountered two children with the same key, `a`.');
});
it('does not call lazy initializers eagerly', () => {
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 8bb8df8736c0b..2bd4e0798911c 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -84,7 +84,7 @@ export const disableStringRefs = false;
export const enableFastJSX = false;
export const enableReactTestRendererWarning = false;
-export const disableLegacyMode = false;
+export const disableLegacyMode = true;
export const disableDefaultPropsExceptForClasses = false;
export const enableAddPropertiesFastPath = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index 7ad7c293f2eef..4ec863c7bbb84 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -32,6 +32,7 @@ export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const enableAddPropertiesFastPath = __VARIANT__;
+export const disableLegacyMode = __VARIANT__;
// Enable this flag to help with concurrent mode debugging.
// It logs information to the console about React scheduling, rendering, and commit phases.
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 25064d60e9b2c..9404b877f532b 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -119,7 +119,8 @@ export const useModernStrictMode = true;
// because JSX is an extremely hot path.
export const disableStringRefs = false;
-export const disableLegacyMode = __EXPERIMENTAL__;
+export const disableLegacyMode: boolean =
+ __EXPERIMENTAL__ || dynamicFeatureFlags.disableLegacyMode;
export const enableOwnerStacks = false;
export const enableShallowPropDiffing = false;