Update on "[compiler][fixtures] test repros: codegen, alignScope, phis"

The AlignReactiveScope bug should be simplest to fix, but it's also caught by an invariant assertion. I think a fix could be either keeping track of "active" block-fallthrough pairs (`retainWhere(pair => pair.range.end > current.instr[0].id)`) or following the approach in `assertValidBlockNesting`.
I'm tempted to pull the value-block aligning logic out into its own pass (using the current `node` tree traversal), then align to non-value blocks with the `assertValidBlockNesting` approach. Happy to hear feedback on this though!

The other two are likely bigger issues, as they're not caught by static invariants.

Update:
- removed bug-phi-reference-effect as it's been patched by josephsavona
- added bug-array-concat-should-capture

[ghstack-poisoned]
This commit is contained in:
Mofei Zhang
2024-06-13 12:47:42 -04:00
33 changed files with 361 additions and 713 deletions
+6
View File
@@ -14,6 +14,12 @@ module.exports = {
parser: 'flow',
arrowParens: 'avoid',
overrides: [
{
files: ['*.code-workspace'],
options: {
parser: 'json-stringify',
},
},
{
files: esNextPaths,
options: {
@@ -1668,6 +1668,15 @@ function lowerExpression(
const left = lowerExpressionToTemporary(builder, leftPath);
const right = lowerExpressionToTemporary(builder, expr.get("right"));
const operator = expr.node.operator;
if (operator === "|>") {
builder.errors.push({
reason: `(BuildHIR::lowerExpression) Pipe operator not supported`,
severity: ErrorSeverity.Todo,
loc: leftPath.node.loc ?? null,
suggestions: null,
});
return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
}
return {
kind: "BinaryExpression",
operator,
@@ -1893,7 +1902,9 @@ function lowerExpression(
);
}
const operators: { [key: string]: t.BinaryExpression["operator"] } = {
const operators: {
[key: string]: Exclude<t.BinaryExpression["operator"], "|>">;
} = {
"+=": "+",
"-=": "-",
"/=": "/",
@@ -2307,6 +2318,20 @@ function lowerExpression(
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
}
} else if (expr.node.operator === "throw") {
builder.errors.push({
reason: `Throw expressions are not supported`,
severity: ErrorSeverity.InvalidJS,
loc: expr.node.loc ?? null,
suggestions: [
{
description: "Remove this line",
range: [expr.node.start!, expr.node.end!],
op: CompilerSuggestionOperation.Remove,
},
],
});
return { kind: "UnsupportedNode", node: expr.node, loc: exprLoc };
} else {
return {
kind: "UnaryExpression",
@@ -866,7 +866,7 @@ export type InstructionValue =
| JSXText
| {
kind: "BinaryExpression";
operator: t.BinaryExpression["operator"];
operator: Exclude<t.BinaryExpression["operator"], "|>">;
left: Place;
right: Place;
loc: SourceLocation;
@@ -881,7 +881,7 @@ export type InstructionValue =
| MethodCall
| {
kind: "UnaryExpression";
operator: t.UnaryExpression["operator"];
operator: Exclude<t.UnaryExpression["operator"], "throw" | "delete">;
value: Place;
loc: SourceLocation;
}
@@ -856,7 +856,7 @@ export function mapTerminalSuccessors(
const block = fn(terminal.block);
const fallthrough = fn(terminal.fallthrough);
return {
kind: "scope",
kind: terminal.kind,
scope: terminal.scope,
block,
fallthrough,
@@ -201,7 +201,7 @@ export default function inferReferenceEffects(
let queuedState = queuedStates.get(blockId);
if (queuedState != null) {
// merge the queued states for this block
state = queuedState.merge(state) ?? state;
state = queuedState.merge(state) ?? queuedState;
queuedStates.set(blockId, state);
} else {
/*
@@ -765,7 +765,7 @@ class InferenceState {
result.values[id] = { kind, value: printMixedHIR(value) };
}
for (const [variable, values] of this.#variables) {
result.variables[variable] = [...values].map(identify);
result.variables[`$${variable}`] = [...values].map(identify);
}
return result;
}
@@ -0,0 +1,72 @@
## Input
```javascript
import { mutate } from "shared-runtime";
/**
* Fixture showing why `concat` needs to capture both the callee and rest args.
* Here, observe that arr1's values are captured into arr2.
* - Later mutations of arr2 may write to values within arr1.
* - Observe that it's technically valid to separately memoize the array arr1
* itself.
*/
function Foo({ inputNum }) {
const arr1: Array<number | object> = [{ a: 1 }, {}];
const arr2 = arr1.concat([1, inputNum]);
mutate(arr2[0]);
return arr2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ inputNum: 2 }],
sequentialRenders: [{ inputNum: 2 }, { inputNum: 3 }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { mutate } from "shared-runtime";
/**
* Fixture showing why `concat` needs to capture both the callee and rest args.
* Here, observe that arr1's values are captured into arr2.
* - Later mutations of arr2 may write to values within arr1.
* - Observe that it's technically valid to separately memoize the array arr1
* itself.
*/
function Foo(t0) {
const $ = _c(3);
const { inputNum } = t0;
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [{ a: 1 }, {}];
$[0] = t1;
} else {
t1 = $[0];
}
const arr1 = t1;
let arr2;
if ($[1] !== inputNum) {
arr2 = arr1.concat([1, inputNum]);
mutate(arr2[0]);
$[1] = inputNum;
$[2] = arr2;
} else {
arr2 = $[2];
}
return arr2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ inputNum: 2 }],
sequentialRenders: [{ inputNum: 2 }, { inputNum: 3 }],
};
```
@@ -0,0 +1,21 @@
import { mutate } from "shared-runtime";
/**
* Fixture showing why `concat` needs to capture both the callee and rest args.
* Here, observe that arr1's values are captured into arr2.
* - Later mutations of arr2 may write to values within arr1.
* - Observe that it's technically valid to separately memoize the array arr1
* itself.
*/
function Foo({ inputNum }) {
const arr1: Array<number | object> = [{ a: 1 }, {}];
const arr2 = arr1.concat([1, inputNum]);
mutate(arr2[0]);
return arr2;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ inputNum: 2 }],
sequentialRenders: [{ inputNum: 2 }, { inputNum: 3 }],
};
@@ -1,78 +0,0 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
/**
* Evaluator error:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [2]
* [2]
* Forget:
* (kind: ok) [2]
* [2,2]
*/
function Foo(cond) {
let x = null;
if (cond) {
x = [];
} else {
}
// Here, x = phi(x$null, x$[]) does not receive the correct ValueKind
arrayPush(x, 2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
/**
* Evaluator error:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [2]
* [2]
* Forget:
* (kind: ok) [2]
* [2,2]
*/
function Foo(cond) {
const $ = _c(1);
let x = null;
if (cond) {
let t0;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t0 = [];
$[0] = t0;
} else {
t0 = $[0];
}
x = t0;
}
arrayPush(x, 2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
@@ -0,0 +1,61 @@
## Input
```javascript
import { arrayPush } from "shared-runtime";
function Foo(cond) {
let x = null;
if (cond) {
x = [];
} else {
}
// Here, x = phi(x$null, x$[]) should receive a ValueKind of Mutable
arrayPush(x, 2);
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { arrayPush } from "shared-runtime";
function Foo(cond) {
const $ = _c(2);
let x;
if ($[0] !== cond) {
x = null;
if (cond) {
x = [];
}
arrayPush(x, 2);
$[0] = cond;
$[1] = x;
} else {
x = $[1];
}
return x;
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: true }],
sequentialRenders: [{ cond: true }, { cond: true }],
};
```
### Eval output
(kind: ok) [2]
[2]
@@ -1,22 +1,12 @@
import { arrayPush } from "shared-runtime";
/**
* Evaluator error:
* Found differences in evaluator results
* Non-forget (expected):
* (kind: ok) [2]
* [2]
* Forget:
* (kind: ok) [2]
* [2,2]
*/
function Foo(cond) {
let x = null;
if (cond) {
x = [];
} else {
}
// Here, x = phi(x$null, x$[]) does not receive the correct ValueKind
// Here, x = phi(x$null, x$[]) should receive a ValueKind of Mutable
arrayPush(x, 2);
return x;
@@ -489,7 +489,7 @@ const skipFilter = new Set([
"original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",
"original-reactive-scopes-fork/bug-hoisted-declaration-with-scope",
"bug-codegen-inline-iife",
"bug-phi-reference-effect",
"bug-array-concat-should-capture",
// 'react-compiler-runtime' not yet supported
"flag-enable-emit-hook-guards",
@@ -36,7 +36,7 @@ export const CONST_NUMBER2 = 2;
export const CONST_TRUE = true;
export const CONST_FALSE = false;
export function initFbt() {
export function initFbt(): void {
const viewerContext: IntlViewerContext = {
GENDER: IntlVariations.GENDER_UNKNOWN,
locale: "en_US",
@@ -52,7 +52,7 @@ export function initFbt() {
export function mutate(arg: any): void {
// don't mutate primitive
if (typeof arg === null || typeof arg !== "object") {
if (arg == null || typeof arg !== "object") {
return;
}
@@ -80,7 +80,7 @@ export function mutateAndReturnNewValue<T>(arg: T): string {
export function setProperty(arg: any, property: any): void {
// don't mutate primitive
if (typeof arg === null || typeof arg !== "object") {
if (arg == null || typeof arg !== "object") {
return arg;
}
@@ -123,7 +123,7 @@ export function calculateExpensiveNumber(x: number): number {
/**
* Functions that do not mutate their parameters
*/
export function shallowCopy(obj: Object): object {
export function shallowCopy(obj: object): object {
return Object.assign({}, obj);
}
@@ -139,9 +139,11 @@ export function addOne(value: number): number {
return value + 1;
}
// Alias console.log, as it is defined as a global and may have
// different compiler handling than unknown functions
export function print(...args: Array<unknown>) {
/*
* Alias console.log, as it is defined as a global and may have
* different compiler handling than unknown functions
*/
export function print(...args: Array<unknown>): void {
console.log(...args);
}
@@ -153,7 +155,7 @@ export function throwErrorWithMessage(message: string): never {
throw new Error(message);
}
export function throwInput(x: Object): never {
export function throwInput(x: object): never {
throw x;
}
@@ -167,12 +169,12 @@ export function logValue<T>(value: T): void {
console.log(value);
}
export function useHook(): Object {
export function useHook(): object {
return makeObject_Primitives();
}
const noAliasObject = Object.freeze({});
export function useNoAlias(...args: Array<any>): object {
export function useNoAlias(..._args: Array<any>): object {
return noAliasObject;
}
@@ -183,7 +185,7 @@ export function useIdentity<T>(arg: T): T {
export function invoke<T extends Array<any>, ReturnType>(
fn: (...input: T) => ReturnType,
...params: T
) {
): ReturnType {
return fn(...params);
}
@@ -191,7 +193,7 @@ export function conditionalInvoke<T extends Array<any>, ReturnType>(
shouldInvoke: boolean,
fn: (...input: T) => ReturnType,
...params: T
) {
): ReturnType | null {
if (shouldInvoke) {
return fn(...params);
} else {
@@ -205,21 +207,25 @@ export function conditionalInvoke<T extends Array<any>, ReturnType>(
export function Text(props: {
value: string;
children?: Array<React.ReactNode>;
}) {
}): React.ReactElement {
return React.createElement("div", null, props.value, props.children);
}
export function StaticText1(props: { children?: Array<React.ReactNode> }) {
export function StaticText1(props: {
children?: Array<React.ReactNode>;
}): React.ReactElement {
return React.createElement("div", null, "StaticText1", props.children);
}
export function StaticText2(props: { children?: Array<React.ReactNode> }) {
export function StaticText2(props: {
children?: Array<React.ReactNode>;
}): React.ReactElement {
return React.createElement("div", null, "StaticText2", props.children);
}
export function RenderPropAsChild(props: {
items: Array<() => React.ReactNode>;
}) {
}): React.ReactElement {
return React.createElement(
"div",
null,
@@ -242,7 +248,7 @@ export function ValidateMemoization({
}: {
inputs: Array<any>;
output: any;
}) {
}): React.ReactElement {
"use no forget";
const [previousInputs, setPreviousInputs] = React.useState(inputs);
const [previousOutput, setPreviousOutput] = React.useState(output);
@@ -273,7 +279,7 @@ export function createHookWrapper<TProps, TRet>(
}
// helper functions
export function toJSON(value: any, invokeFns: boolean = false) {
export function toJSON(value: any, invokeFns: boolean = false): string {
const seen = new Map();
return JSON.stringify(value, (_key: string, val: any) => {
@@ -319,7 +325,7 @@ export const ObjectWithHooks = {
},
};
export function useFragment(...args: Array<any>): Object {
export function useFragment(..._args: Array<any>): object {
return {
a: [1, 2, 3],
b: { c: { d: 4 } },
+8 -8
View File
@@ -79,11 +79,11 @@ brace-expansion@^1.1.7:
concat-map "0.0.1"
braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.0.1"
fill-range "^7.1.1"
browserslist@^4.18.1:
version "4.21.7"
@@ -265,10 +265,10 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
+1
View File
@@ -280,6 +280,7 @@ function initialize(socket: WebSocket) {
store = new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
supportsClickToInspect: true,
});
log('Connected');
+2 -1
View File
@@ -97,7 +97,8 @@ function createBridgeAndStore() {
// At this time, the timeline can only parse Chrome performance profiles.
supportsTimeline: __IS_CHROME__,
supportsTraceUpdates: true,
supportsNativeInspection: true,
supportsInspectMatchingDOMElement: true,
supportsClickToInspect: true,
});
if (!isProfiling) {
+1
View File
@@ -37,6 +37,7 @@ export function createStore(bridge: FrontendBridge, config?: Config): Store {
return new Store(bridge, {
checkBridgeProtocolCompatibility: true,
supportsTraceUpdates: true,
supportsClickToInspect: true,
...config,
});
}
+17 -7
View File
@@ -71,7 +71,8 @@ type ErrorAndWarningTuples = Array<{id: number, index: number}>;
export type Config = {
checkBridgeProtocolCompatibility?: boolean,
isProfiling?: boolean,
supportsNativeInspection?: boolean,
supportsInspectMatchingDOMElement?: boolean,
supportsClickToInspect?: boolean,
supportsReloadAndProfile?: boolean,
supportsTimeline?: boolean,
supportsTraceUpdates?: boolean,
@@ -172,7 +173,8 @@ export default class Store extends EventEmitter<{
_rootIDToRendererID: Map<number, number> = new Map();
// These options may be initially set by a configuration option when constructing the Store.
_supportsNativeInspection: boolean = false;
_supportsInspectMatchingDOMElement: boolean = false;
_supportsClickToInspect: boolean = false;
_supportsReloadAndProfile: boolean = false;
_supportsTimeline: boolean = false;
_supportsTraceUpdates: boolean = false;
@@ -211,13 +213,17 @@ export default class Store extends EventEmitter<{
isProfiling = config.isProfiling === true;
const {
supportsNativeInspection,
supportsInspectMatchingDOMElement,
supportsClickToInspect,
supportsReloadAndProfile,
supportsTimeline,
supportsTraceUpdates,
} = config;
if (supportsNativeInspection) {
this._supportsNativeInspection = true;
if (supportsInspectMatchingDOMElement) {
this._supportsInspectMatchingDOMElement = true;
}
if (supportsClickToInspect) {
this._supportsClickToInspect = true;
}
if (supportsReloadAndProfile) {
this._supportsReloadAndProfile = true;
@@ -437,8 +443,12 @@ export default class Store extends EventEmitter<{
return this._rootSupportsTimelineProfiling;
}
get supportsNativeInspection(): boolean {
return this._supportsNativeInspection;
get supportsInspectMatchingDOMElement(): boolean {
return this._supportsInspectMatchingDOMElement;
}
get supportsClickToInspect(): boolean {
return this._supportsClickToInspect;
}
get supportsNativeStyleEditor(): boolean {
@@ -296,7 +296,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
<ButtonIcon type="suspend" />
</Toggle>
)}
{store.supportsNativeInspection && (
{store.supportsInspectMatchingDOMElement && (
<Button
onClick={highlightElement}
title="Inspect the matching DOM element">
@@ -361,7 +361,7 @@ export default function Tree(props: Props): React.Node {
<TreeFocusedContext.Provider value={treeFocused}>
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
{store.supportsNativeInspection && (
{store.supportsClickToInspect && (
<Fragment>
<InspectHostNodesToggle />
<div className={styles.VRule} />
@@ -52,6 +52,7 @@ import {
enableLegacyFBSupport,
enableCreateEventHandleAPI,
enableScopeAPI,
enableOwnerStacks,
} from 'shared/ReactFeatureFlags';
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
import {
@@ -70,6 +71,8 @@ import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';
import reportGlobalError from 'shared/reportGlobalError';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
type DispatchListener = {
instance: null | Fiber,
listener: Function,
@@ -255,7 +258,17 @@ function processDispatchQueueItemsInOrder(
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
} else {
@@ -264,7 +277,17 @@ function processDispatchQueueItemsInOrder(
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(
instance,
executeDispatch,
event,
listener,
currentTarget,
);
} else {
executeDispatch(event, listener, currentTarget);
}
previousInstance = instance;
}
}
+2 -2
View File
@@ -63,9 +63,9 @@
"bun": "./server.bun.js",
"deno": "./server.browser.js",
"worker": "./server.browser.js",
"browser": "./server.browser.js",
"node": "./server.node.js",
"edge-light": "./server.edge.js",
"browser": "./server.browser.js",
"default": "./server.node.js"
},
"./server.browser": {
@@ -89,9 +89,9 @@
"workerd": "./static.edge.js",
"deno": "./static.browser.js",
"worker": "./static.browser.js",
"browser": "./static.browser.js",
"node": "./static.node.js",
"edge-light": "./static.edge.js",
"browser": "./static.browser.js",
"default": "./static.node.js"
},
"./static.browser": {
-1
View File
@@ -43,6 +43,5 @@ export {
render,
unstable_batchedUpdates,
findDOMNode,
unstable_renderSubtreeIntoContainer,
unmountComponentAtNode,
} from './client/ReactDOMRootFB';
@@ -1,353 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const React = require('react');
const PropTypes = require('prop-types');
const ReactDOM = require('react-dom');
const ReactDOMClient = require('react-dom/client');
const act = require('internal-test-utils').act;
const renderSubtreeIntoContainer =
require('react-dom').unstable_renderSubtreeIntoContainer;
describe('renderSubtreeIntoContainer', () => {
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should pass context when rendering subtree elsewhere', () => {
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.foo}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return null;
}
componentDidMount() {
expect(
function () {
renderSubtreeIntoContainer(this, <Component />, portal);
}.bind(this),
).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
expect(portal.firstChild.innerHTML).toBe('bar');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to setState', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
render() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
state = {
bar: 'initial',
};
getChildContext() {
return {
foo: this.state.bar,
getFoo: () => this.state.bar,
};
}
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
componentDidUpdate() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
const parentRef = React.createRef();
await act(async () => {
root.render(<Parent ref={parentRef} />);
});
const instance = parentRef.current;
expect(portal.firstChild.innerHTML).toBe('initial-initial');
await act(async () => {
instance.setState({bar: 'changed'});
});
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to re-render', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
render() {
return <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
getFoo: PropTypes.func.isRequired,
};
getChildContext() {
return {
foo: this.props.bar,
getFoo: () => this.props.bar,
};
}
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
componentDidUpdate() {
expect(() => {
renderSubtreeIntoContainer(this, <Component />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent bar="initial" />);
});
expect(portal.firstChild.innerHTML).toBe('initial-initial');
await act(async () => {
root.render(<Parent bar="changed" />);
});
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
// @gate !disableLegacyMode
it('should render portal with non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Parent extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <div>hello</div>, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent bar="initial" />);
});
expect(portal.firstChild.innerHTML).toBe('hello');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal = document.createElement('div');
class Parent extends React.Component {
render() {
return <Middle />;
}
getChildContext() {
return {value: this.props.value};
}
static childContextTypes = {
value: PropTypes.string.isRequired,
};
}
class Middle extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Child />, portal);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
class Child extends React.Component {
static contextTypes = {
value: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.value}</div>;
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent value="foo" />);
});
expect(portal.textContent).toBe('foo');
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through middle non-context-provider layer', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
const portal1 = document.createElement('div');
const portal2 = document.createElement('div');
class Parent extends React.Component {
render() {
return null;
}
getChildContext() {
return {value: this.props.value};
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Middle />, portal1);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
static childContextTypes = {
value: PropTypes.string.isRequired,
};
}
class Middle extends React.Component {
render() {
return null;
}
componentDidMount() {
expect(() => {
renderSubtreeIntoContainer(this, <Child />, portal2);
}).toErrorDev(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported since React 18',
);
}
}
class Child extends React.Component {
static contextTypes = {
value: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.value}</div>;
}
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<Parent value="foo" />);
});
expect(portal2.textContent).toBe('foo');
});
// @gate !disableLegacyMode
it('legacy test: fails gracefully when mixing React 15 and 16', () => {
class C extends React.Component {
render() {
return <div />;
}
}
const c = ReactDOM.render(<C />, document.createElement('div'));
// React 15 calls this:
// https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479
expect(() => {
c._reactInternalInstance._processChildContext({});
}).toThrow(
__DEV__
? '_processChildContext is not available in React 16+. This likely ' +
'means you have multiple copies of React and are attempting to nest ' +
'a React 15 tree inside a React 16 tree using ' +
"unstable_renderSubtreeIntoContainer, which isn't supported. Try to " +
'make sure you have only one copy of React (and ideally, switch to ' +
'ReactDOM.createPortal).'
: "Cannot read property '_processChildContext' of undefined",
);
});
});
-41
View File
@@ -59,7 +59,6 @@ import {
} from 'react-reconciler/src/ReactFiberReconciler';
import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import {has as hasInstance} from 'shared/ReactInstanceMap';
import {
current as currentOwner,
@@ -420,46 +419,6 @@ export function render(
);
}
export function unstable_renderSubtreeIntoContainer(
parentComponent: React$Component<any, any>,
element: React$Element<any>,
containerNode: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (disableLegacyMode) {
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() was removed in React 19. Consider using a portal instead.',
);
}
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
}
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported ' +
'since React 18. Consider using a portal instead. Until you switch to ' +
"the createRoot API, your app will behave as if it's running React " +
'17. Learn more: https://react.dev/link/switch-to-createroot',
);
}
if (!isValidContainerLegacy(containerNode)) {
throw new Error('Target container is not a DOM element.');
}
if (parentComponent == null || !hasInstance(parentComponent)) {
throw new Error('parentComponent must be a valid React Component');
}
return legacyRenderSubtreeIntoContainer(
parentComponent,
element,
containerNode,
false,
callback,
);
}
export function unmountComponentAtNode(container: Container): boolean {
if (disableLegacyMode) {
if (__DEV__) {
@@ -7,6 +7,10 @@
import isArray from 'shared/isArray';
import {enableOwnerStacks} from 'shared/ReactFeatureFlags';
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
let hasError = false;
let caughtError = null;
@@ -93,10 +97,22 @@ export function executeDispatchesInOrder(event) {
break;
}
// Listeners and Instances are two parallel arrays that are always in sync.
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
const listener = dispatchListeners[i];
const instance = dispatchInstances[i];
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(instance, executeDispatch, event, listener, instance);
} else {
executeDispatch(event, listener, instance);
}
}
} else if (dispatchListeners) {
executeDispatch(event, dispatchListeners, dispatchInstances);
const listener = dispatchListeners;
const instance = dispatchInstances;
if (__DEV__ && enableOwnerStacks && instance !== null) {
runWithFiberInDEV(instance, executeDispatch, event, listener, instance);
} else {
executeDispatch(event, listener, instance);
}
}
event._dispatchListeners = null;
event._dispatchInstances = null;
+1 -21
View File
@@ -73,9 +73,7 @@ import {
setIsStrictModeForDevtools,
} from './ReactFiberDevToolsHook';
const fakeInternalInstance: {
_processChildContext?: () => empty,
} = {};
const fakeInternalInstance = {};
let didWarnAboutStateAssignmentForComponent;
let didWarnAboutUninitializedState;
@@ -98,24 +96,6 @@ if (__DEV__) {
didWarnAboutInvalidateContextType = new Set<string>();
didWarnOnInvalidCallback = new Set<string>();
// This is so gross but it's at least non-critical and can be removed if
// it causes problems. This is meant to give a nicer error message for
// ReactDOM15.unstable_renderSubtreeIntoContainer(reactDOM16Component,
// ...)) which otherwise throws a "_processChildContext is not a function"
// exception.
Object.defineProperty(fakeInternalInstance, '_processChildContext', {
enumerable: false,
value: function (): empty {
throw new Error(
'_processChildContext is not available in React 16+. This likely ' +
'means you have multiple copies of React and are attempting to nest ' +
'a React 15 tree inside a React 16 tree using ' +
"unstable_renderSubtreeIntoContainer, which isn't supported. Try " +
'to make sure you have only one copy of React (and ideally, switch ' +
'to ReactDOM.createPortal).',
);
},
});
Object.freeze(fakeInternalInstance);
}
@@ -1,99 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from '../ReactServerStreamConfigFB';
import type {
PrecomputedChunk,
Chunk,
BinaryChunk,
} from '../ReactServerStreamConfigFB';
let byteLengthImpl: null | ((chunk: Chunk | PrecomputedChunk) => number) = null;
export function setByteLengthOfChunkImplementation(
impl: (chunk: Chunk | PrecomputedChunk) => number,
): void {
byteLengthImpl = impl;
}
export function byteLengthOfChunk(chunk: Chunk | PrecomputedChunk): number {
if (byteLengthImpl == null) {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'byteLengthOfChunk implementation is not configured. Please, provide the implementation via ReactFlightDOMServer.setConfig(...);',
);
}
return byteLengthImpl(chunk);
}
export interface Destination {
beginWriting(): void;
write(chunk: Chunk | PrecomputedChunk | BinaryChunk): void;
completeWriting(): void;
flushBuffered(): void;
close(): void;
onError(error: mixed): void;
}
function handleErrorInNextTick(error: any) {
setTimeout(() => {
throw error;
});
}
const LocalPromise = Promise;
/**
* Since this environment doesn't have a way to schedule tasks from JS we schedule
* using a microtask instead. This isn't necessarily ideal since we would like to give
* other IO a chance to run before performing work typically but it's the best we can
* do in this environment
*/
export function scheduleWork(callback: () => void) {
LocalPromise.resolve().then(callback).catch(handleErrorInNextTick);
}
export const scheduleMicrotask: (callback: () => void) => void = scheduleWork;
export function beginWriting(destination: Destination) {
destination.beginWriting();
}
export function writeChunk(
destination: Destination,
chunk: Chunk | PrecomputedChunk | BinaryChunk,
): void {
destination.write(chunk);
}
export function writeChunkAndReturn(
destination: Destination,
chunk: Chunk | PrecomputedChunk | BinaryChunk,
): boolean {
destination.write(chunk);
return true;
}
export function completeWriting(destination: Destination) {
destination.completeWriting();
}
export function flushBuffered(destination: Destination) {
destination.flushBuffered();
}
export function close(destination: Destination) {
destination.close();
}
export function closeWithError(destination: Destination, error: mixed): void {
destination.onError(error);
destination.close();
}
-13
View File
@@ -15,23 +15,10 @@
* If this becomes an actual Map, that will break.
*/
/**
* This API should be called `delete` but we'd have to make sure to always
* transform these to strings for IE support. When this transform is fully
* supported we can rename it.
*/
export function remove(key) {
key._reactInternals = undefined;
}
export function get(key) {
return key._reactInternals;
}
export function has(key) {
return key._reactInternals !== undefined;
}
export function set(key, value) {
key._reactInternals = value;
}
@@ -20,7 +20,7 @@ let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
let originalError;
let assertConsoleErrorDev;
// This tests shared behavior between the built-in and shim implementations of
// of useSyncExternalStore.
@@ -50,9 +50,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
: 'react-dom-17/umd/react-dom.production.min.js',
),
);
// Because React 17 prints extra logs we need to ignore them.
originalError = console.error;
console.error = jest.fn();
}
React = require('react');
ReactDOM = require('react-dom');
@@ -63,6 +60,7 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useLayoutEffect = React.useLayoutEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
const internalAct = require('internal-test-utils').act;
// The internal act implementation doesn't batch updates by default, since
@@ -85,11 +83,6 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
useSyncExternalStoreWithSelector =
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
});
afterEach(() => {
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
console.error = originalError;
}
});
function Text({text}) {
Scheduler.log(text);
return text;
@@ -630,36 +623,30 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
const container = document.createElement('div');
const root = createRoot(container);
await expect(async () => {
await expect(async () => {
await act(() => {
ReactDOM.flushSync(async () =>
root.render(React.createElement(App, null)),
);
});
}).rejects.toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
}).toErrorDev(
await act(() => {
ReactDOM.flushSync(async () =>
root.render(React.createElement(App, null)),
);
});
}).rejects.toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
assertConsoleErrorDev(
gate(flags => flags.enableUseSyncExternalStoreShim)
? [
'Maximum update depth exceeded. ',
'The result of getSnapshot should be cached to avoid an infinite loop',
'The above error occurred in the',
[
'The result of getSnapshot should be cached to avoid an infinite loop',
{withoutStack: true},
],
'Error: Maximum update depth exceeded',
'The above error occurred i',
]
: [
'The result of getSnapshot should be cached to avoid an infinite loop',
],
{
withoutStack: gate(flags => {
if (flags.enableUseSyncExternalStoreShim) {
// Stacks don't work when mixing the source and the npm package.
return flags.source ? 1 : 0;
}
return false;
}),
},
);
});
it('getSnapshot can return NaN without infinite loop warning', async () => {
@@ -850,10 +837,9 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
// client. To avoid this server mismatch warning, user must account for
// this themselves and return the correct value inside `getSnapshot`.
await act(() => {
expect(() =>
ReactDOM.hydrate(React.createElement(App, null), container),
).toErrorDev('Text content did not match');
ReactDOM.hydrate(React.createElement(App, null), container);
});
assertConsoleErrorDev(['Text content did not match']);
assertLog(['client', 'Passive effect: client']);
}
expect(container.textContent).toEqual('client');
@@ -16,7 +16,10 @@ import * as React from 'react';
export const useSyncExternalStore = React.useSyncExternalStore;
if (__DEV__) {
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
"The main 'use-sync-external-store' entry point is not supported; all it " +
"does is re-export useSyncExternalStore from the 'react' package, so " +
'it only works with React 18+.' +
@@ -40,7 +40,10 @@ export function useSyncExternalStore<T>(
if (!didWarnOld18Alpha) {
if (React.startTransition !== undefined) {
didWarnOld18Alpha = true;
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
'You are using an outdated, pre-release alpha of React 18 that ' +
'does not support useSyncExternalStore. The ' +
'use-sync-external-store shim will not work correctly. Upgrade ' +
@@ -59,7 +62,10 @@ export function useSyncExternalStore<T>(
if (!didWarnUncachedGetSnapshot) {
const cachedValue = getSnapshot();
if (!is(value, cachedValue)) {
console.error(
// Avoid transforming the `console.error` call as it would cause the built artifact
// to access React internals, which exist under different paths depending on the
// React version.
console['error'](
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
+30
View File
@@ -0,0 +1,30 @@
{
"folders": [
{
"path": "."
}
],
"extensions": {
"recommendations": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"flowtype.flow-for-vscode"
]
},
"settings": {
"search.exclude": {
"**/dist/**": true,
"**/build/**": true,
"**/out/**": true,
"*.map": true,
"*.log": true
},
"javascript.validate.enable": false,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"flow.pathToFlow": "${workspaceFolder}/node_modules/.bin/flow",
"prettier.configPath": "",
"prettier.ignorePath": ""
}
}
+2 -7
View File
@@ -95,13 +95,8 @@ function getTestFlags() {
// This is used by useSyncExternalStoresShared-test.js to decide whether
// to test the shim or the native implementation of useSES.
// TODO: It's disabled when enableRefAsProp is on because the JSX
// runtime used by our tests is not compatible with older versions of
// React. If we want to keep testing this shim after enableRefIsProp is
// on everywhere, we'll need to find some other workaround. Maybe by
// only using createElement instead of JSX in that test module.
enableUseSyncExternalStoreShim:
!__VARIANT__ && !featureFlags.enableRefAsProp,
enableUseSyncExternalStoreShim: !__VARIANT__,
// If there's a naming conflict between scheduler and React feature flags, the
// React ones take precedence.