Update base for Update on "[compiler] Infer optional manual memo deps"

When inferring dependencies of manual memoization in DropManualMemo, we now infer which parts of a dependency path were from optional member expressions.

[ghstack-poisoned]
This commit is contained in:
Joe Savona
2024-08-28 10:44:46 -07:00
18 changed files with 805 additions and 556 deletions
@@ -87,7 +87,7 @@ jobs:
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Insert @headers into eslint plugin and react-refresh
run: |
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n * \n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Move relevant files for React in www into compiled
@@ -11,12 +11,12 @@ import {
IdentifierId,
Place,
SourceLocation,
isRefOrRefValue,
isRefValueType,
isUseRefType,
} from '../HIR';
import {
eachInstructionValueOperand,
eachPatternOperand,
eachTerminalOperand,
} from '../HIR/visitors';
import {Err, Ok, Result} from '../Utils/Result';
@@ -42,58 +42,165 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies';
* In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
* or based on property name alone (`foo.current` might be a ref).
*/
type State = {
refs: Set<IdentifierId>;
refValues: Map<IdentifierId, SourceLocation | null>;
refAccessingFunctions: Set<IdentifierId>;
};
export function validateNoRefAccessInRender(fn: HIRFunction): void {
const refAccessingFunctions: Set<IdentifierId> = new Set();
validateNoRefAccessInRenderImpl(fn, refAccessingFunctions).unwrap();
const state = {
refs: new Set<IdentifierId>(),
refValues: new Map<IdentifierId, SourceLocation | null>(),
refAccessingFunctions: new Set<IdentifierId>(),
};
validateNoRefAccessInRenderImpl(fn, state).unwrap();
}
function validateNoRefAccessInRenderImpl(
fn: HIRFunction,
refAccessingFunctions: Set<IdentifierId>,
state: State,
): Result<void, CompilerError> {
let place;
for (const param of fn.params) {
if (param.kind === 'Identifier') {
place = param;
} else {
place = param.place;
}
if (isRefValueType(place.identifier)) {
state.refValues.set(place.identifier.id, null);
}
if (isUseRefType(place.identifier)) {
state.refs.add(place.identifier.id);
}
}
const errors = new CompilerError();
const lookupLocations: Map<IdentifierId, SourceLocation> = new Map();
for (const [, block] of fn.body.blocks) {
for (const phi of block.phis) {
phi.operands.forEach(operand => {
if (state.refs.has(operand.id) || isUseRefType(phi.id)) {
state.refs.add(phi.id.id);
}
const refValue = state.refValues.get(operand.id);
if (refValue !== undefined || isRefValueType(operand)) {
state.refValues.set(
phi.id.id,
refValue ?? state.refValues.get(phi.id.id) ?? null,
);
}
if (state.refAccessingFunctions.has(operand.id)) {
state.refAccessingFunctions.add(phi.id.id);
}
});
}
for (const instr of block.instructions) {
for (const operand of eachInstructionValueOperand(instr.value)) {
if (isRefValueType(operand.identifier)) {
CompilerError.invariant(state.refValues.has(operand.identifier.id), {
reason: 'Expected ref value to be in state',
loc: operand.loc,
});
}
if (isUseRefType(operand.identifier)) {
CompilerError.invariant(state.refs.has(operand.identifier.id), {
reason: 'Expected ref to be in state',
loc: operand.loc,
});
}
}
switch (instr.value.kind) {
case 'JsxExpression':
case 'JsxFragment': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoDirectRefValueAccess(errors, operand, lookupLocations);
validateNoDirectRefValueAccess(errors, operand, state);
}
break;
}
case 'ComputedLoad':
case 'PropertyLoad': {
if (typeof instr.value.property !== 'string') {
validateNoRefValueAccess(errors, state, instr.value.property);
}
if (
isRefValueType(instr.lvalue.identifier) &&
instr.value.property === 'current'
state.refAccessingFunctions.has(instr.value.object.identifier.id)
) {
lookupLocations.set(instr.lvalue.identifier.id, instr.loc);
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (state.refs.has(instr.value.object.identifier.id)) {
/*
* Once an object contains a ref at any level, we treat it as a ref.
* If we look something up from it, that value may either be a ref
* or the ref value (or neither), so we conservatively assume it's both.
*/
state.refs.add(instr.lvalue.identifier.id);
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
}
break;
}
case 'LoadContext':
case 'LoadLocal': {
if (refAccessingFunctions.has(instr.value.place.identifier.id)) {
refAccessingFunctions.add(instr.lvalue.identifier.id);
if (
state.refAccessingFunctions.has(instr.value.place.identifier.id)
) {
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (isRefValueType(instr.lvalue.identifier)) {
const loc = lookupLocations.get(instr.value.place.identifier.id);
if (loc !== undefined) {
lookupLocations.set(instr.lvalue.identifier.id, loc);
}
const refValue = state.refValues.get(instr.value.place.identifier.id);
if (refValue !== undefined) {
state.refValues.set(instr.lvalue.identifier.id, refValue);
}
if (state.refs.has(instr.value.place.identifier.id)) {
state.refs.add(instr.lvalue.identifier.id);
}
break;
}
case 'StoreContext':
case 'StoreLocal': {
if (refAccessingFunctions.has(instr.value.value.identifier.id)) {
refAccessingFunctions.add(instr.value.lvalue.place.identifier.id);
refAccessingFunctions.add(instr.lvalue.identifier.id);
if (
state.refAccessingFunctions.has(instr.value.value.identifier.id)
) {
state.refAccessingFunctions.add(
instr.value.lvalue.place.identifier.id,
);
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (isRefValueType(instr.value.lvalue.place.identifier)) {
const loc = lookupLocations.get(instr.value.value.identifier.id);
if (loc !== undefined) {
lookupLocations.set(instr.value.lvalue.place.identifier.id, loc);
lookupLocations.set(instr.lvalue.identifier.id, loc);
const refValue = state.refValues.get(instr.value.value.identifier.id);
if (
refValue !== undefined ||
isRefValueType(instr.value.lvalue.place.identifier)
) {
state.refValues.set(
instr.value.lvalue.place.identifier.id,
refValue ?? null,
);
state.refValues.set(instr.lvalue.identifier.id, refValue ?? null);
}
if (state.refs.has(instr.value.value.identifier.id)) {
state.refs.add(instr.value.lvalue.place.identifier.id);
state.refs.add(instr.lvalue.identifier.id);
}
break;
}
case 'Destructure': {
const destructuredFunction = state.refAccessingFunctions.has(
instr.value.value.identifier.id,
);
const destructuredRef = state.refs.has(
instr.value.value.identifier.id,
);
for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
if (isUseRefType(lval.identifier)) {
state.refs.add(lval.identifier.id);
}
if (destructuredRef || isRefValueType(lval.identifier)) {
state.refs.add(lval.identifier.id);
state.refValues.set(lval.identifier.id, null);
}
if (destructuredFunction) {
state.refAccessingFunctions.add(lval.identifier.id);
}
}
break;
@@ -107,32 +214,27 @@ function validateNoRefAccessInRenderImpl(
*/
[...eachInstructionValueOperand(instr.value)].some(
operand =>
isRefValueType(operand.identifier) ||
refAccessingFunctions.has(operand.identifier.id),
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id),
) ||
// check for cases where .current is accessed through an aliased ref
([...eachInstructionValueOperand(instr.value)].some(operand =>
isUseRefType(operand.identifier),
state.refs.has(operand.identifier.id),
) &&
validateNoRefAccessInRenderImpl(
instr.value.loweredFunc.func,
refAccessingFunctions,
state,
).isErr())
) {
// This function expression unconditionally accesses a ref
refAccessingFunctions.add(instr.lvalue.identifier.id);
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
break;
}
case 'MethodCall': {
if (!isEffectHook(instr.value.property.identifier)) {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefAccess(
errors,
refAccessingFunctions,
operand,
operand.loc,
);
validateNoRefAccess(errors, state, operand, operand.loc);
}
}
break;
@@ -142,7 +244,7 @@ function validateNoRefAccessInRenderImpl(
const isUseEffect = isEffectHook(callee.identifier);
if (!isUseEffect) {
// Report a more precise error when calling a local function that accesses a ref
if (refAccessingFunctions.has(callee.identifier.id)) {
if (state.refAccessingFunctions.has(callee.identifier.id)) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
@@ -159,9 +261,9 @@ function validateNoRefAccessInRenderImpl(
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefAccess(
errors,
refAccessingFunctions,
state,
operand,
lookupLocations.get(operand.identifier.id) ?? operand.loc,
state.refValues.get(operand.identifier.id) ?? operand.loc,
);
}
}
@@ -170,12 +272,17 @@ function validateNoRefAccessInRenderImpl(
case 'ObjectExpression':
case 'ArrayExpression': {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefAccess(
errors,
refAccessingFunctions,
operand,
lookupLocations.get(operand.identifier.id) ?? operand.loc,
);
validateNoDirectRefValueAccess(errors, operand, state);
if (state.refAccessingFunctions.has(operand.identifier.id)) {
state.refAccessingFunctions.add(instr.lvalue.identifier.id);
}
if (state.refs.has(operand.identifier.id)) {
state.refs.add(instr.lvalue.identifier.id);
}
const refValue = state.refValues.get(operand.identifier.id);
if (refValue !== undefined) {
state.refValues.set(instr.lvalue.identifier.id, refValue);
}
}
break;
}
@@ -185,20 +292,15 @@ function validateNoRefAccessInRenderImpl(
case 'ComputedStore': {
validateNoRefAccess(
errors,
refAccessingFunctions,
state,
instr.value.object,
lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc,
state.refValues.get(instr.value.object.identifier.id) ?? instr.loc,
);
for (const operand of eachInstructionValueOperand(instr.value)) {
if (operand === instr.value.object) {
continue;
}
validateNoRefValueAccess(
errors,
refAccessingFunctions,
lookupLocations,
operand,
);
validateNoRefValueAccess(errors, state, operand);
}
break;
}
@@ -207,28 +309,27 @@ function validateNoRefAccessInRenderImpl(
break;
default: {
for (const operand of eachInstructionValueOperand(instr.value)) {
validateNoRefValueAccess(
errors,
refAccessingFunctions,
lookupLocations,
operand,
);
validateNoRefValueAccess(errors, state, operand);
}
break;
}
}
if (isUseRefType(instr.lvalue.identifier)) {
state.refs.add(instr.lvalue.identifier.id);
}
if (
isRefValueType(instr.lvalue.identifier) &&
!state.refValues.has(instr.lvalue.identifier.id)
) {
state.refValues.set(instr.lvalue.identifier.id, instr.loc);
}
}
for (const operand of eachTerminalOperand(block.terminal)) {
if (block.terminal.kind !== 'return') {
validateNoRefValueAccess(
errors,
refAccessingFunctions,
lookupLocations,
operand,
);
validateNoRefValueAccess(errors, state, operand);
} else {
// Allow functions containing refs to be returned, but not direct ref values
validateNoDirectRefValueAccess(errors, operand, lookupLocations);
validateNoDirectRefValueAccess(errors, operand, state);
}
}
}
@@ -242,19 +343,18 @@ function validateNoRefAccessInRenderImpl(
function validateNoRefValueAccess(
errors: CompilerError,
refAccessingFunctions: Set<IdentifierId>,
lookupLocations: Map<IdentifierId, SourceLocation>,
state: State,
operand: Place,
): void {
if (
isRefValueType(operand.identifier) ||
refAccessingFunctions.has(operand.identifier.id)
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
@@ -267,13 +367,14 @@ function validateNoRefValueAccess(
function validateNoRefAccess(
errors: CompilerError,
refAccessingFunctions: Set<IdentifierId>,
state: State,
operand: Place,
loc: SourceLocation,
): void {
if (
isRefOrRefValue(operand.identifier) ||
refAccessingFunctions.has(operand.identifier.id)
state.refs.has(operand.identifier.id) ||
state.refValues.has(operand.identifier.id) ||
state.refAccessingFunctions.has(operand.identifier.id)
) {
errors.push({
severity: ErrorSeverity.InvalidReact,
@@ -293,14 +394,14 @@ function validateNoRefAccess(
function validateNoDirectRefValueAccess(
errors: CompilerError,
operand: Place,
lookupLocations: Map<IdentifierId, SourceLocation>,
state: State,
): void {
if (isRefValueType(operand.identifier)) {
if (state.refValues.has(operand.identifier.id)) {
errors.push({
severity: ErrorSeverity.InvalidReact,
reason:
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
description:
operand.identifier.name !== null &&
operand.identifier.name.kind === 'named'
@@ -22,13 +22,15 @@ function Foo({a}) {
## Error
```
3 | const ref = useRef();
4 | // type information is lost here as we don't track types of fields
> 5 | const val = {ref};
| ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
6 | // without type info, we don't know that val.ref.current is a ref value so we
7 | // *would* end up depending on val.ref.current
8 | // however, this is an instance of accessing a ref during render and is disallowed
8 | // however, this is an instance of accessing a ref during render and is disallowed
9 | // under React's rules, so we reject this input
> 10 | const x = {a, val: val.ref.current};
| ^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
11 |
12 | return <VideoList videos={x} />;
13 | }
```
@@ -0,0 +1,87 @@
## Input
```javascript
// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
import {useRef} from 'react';
component Foo(cond: boolean, cond2: boolean) {
const ref = useRef();
const s = () => {
return ref.current;
};
if (cond) return [s];
else if (cond2) return {s};
else return {s: [s]};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{cond: false, cond2: false}],
};
```
## Code
```javascript
import { c as _c } from "react/compiler-runtime";
import { useRef } from "react";
function Foo(t0) {
const $ = _c(4);
const { cond, cond2 } = t0;
const ref = useRef();
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = () => ref.current;
$[0] = t1;
} else {
t1 = $[0];
}
const s = t1;
if (cond) {
let t2;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = [s];
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
} else {
if (cond2) {
let t2;
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t2 = { s };
$[2] = t2;
} else {
t2 = $[2];
}
return t2;
} else {
let t2;
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
t2 = { s: [s] };
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
}
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{ cond: false, cond2: false }],
};
```
### Eval output
(kind: ok) {"s":["[[ function params=0 ]]"]}
@@ -0,0 +1,20 @@
// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
import {useRef} from 'react';
component Foo(cond: boolean, cond2: boolean) {
const ref = useRef();
const s = () => {
return ref.current;
};
if (cond) return [s];
else if (cond2) return {s};
else return {s: [s]};
}
export const FIXTURE_ENTRYPOINT = {
fn: Foo,
params: [{cond: false, cond2: false}],
};
+3 -13
View File
@@ -21,6 +21,8 @@ import {
setBrowserSelectionFromReact,
setReactSelectionFromBrowser,
} from './elementSelection';
import {viewAttributeSource} from './sourceSelection';
import {startReactPolling} from './reactPolling';
import cloneStyleTags from './cloneStyleTags';
import fetchFileWithCaching from './fetchFileWithCaching';
@@ -113,19 +115,7 @@ function createBridgeAndStore() {
const viewAttributeSourceFunction = (id, path) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID != null) {
// Ask the renderer interface to find the specified attribute,
// and store it as a global variable on the window.
bridge.send('viewAttributeSource', {id, path, rendererID});
setTimeout(() => {
// Ask Chrome to display the location of the attribute,
// assuming the renderer found a match.
chrome.devtools.inspectedWindow.eval(`
if (window.$attribute != null) {
inspect(window.$attribute);
}
`);
}, 100);
viewAttributeSource(rendererID, id, path);
}
};
@@ -0,0 +1,59 @@
/* global chrome */
export function viewAttributeSource(rendererID, elementID, path) {
chrome.devtools.inspectedWindow.eval(
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementAttributeByPath(' +
JSON.stringify(elementID) +
',' +
JSON.stringify(path) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
(didInspect, evalError) => {
if (evalError) {
console.error(evalError);
}
},
);
}
export function viewElementSource(rendererID, elementID) {
chrome.devtools.inspectedWindow.eval(
'{' + // The outer block is important because it means we can declare local variables.
'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
JSON.stringify(rendererID) +
');' +
'if (renderer) {' +
' const value = renderer.getElementSourceFunctionById(' +
JSON.stringify(elementID) +
');' +
' if (value) {' +
' inspect(value);' +
' true;' +
' } else {' +
' false;' +
' }' +
'} else {' +
' false;' +
'}' +
'}',
(didInspect, evalError) => {
if (evalError) {
console.error(evalError);
}
},
);
}
-20
View File
@@ -220,8 +220,6 @@ export default class Agent extends EventEmitter<{
this.updateConsolePatchSettings,
);
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
bridge.addListener('viewAttributeSource', this.viewAttributeSource);
bridge.addListener('viewElementSource', this.viewElementSource);
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
// We do this because React Native embeds the React DevTools backend,
@@ -816,24 +814,6 @@ export default class Agent extends EventEmitter<{
}
};
viewAttributeSource: CopyElementParams => void = ({id, path, rendererID}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.prepareViewAttributeSource(id, path);
}
};
viewElementSource: ElementAndRendererID => void = ({id, rendererID}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.prepareViewElementSource(id);
}
};
onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
this.emit('traceUpdates', nodes);
};
@@ -108,6 +108,23 @@ export function getStackByFiberInDevAndProd(
}
}
export function getSourceLocationByFiber(
workTagMap: WorkTagMap,
fiber: Fiber,
currentDispatcherRef: CurrentDispatcherRef,
): null | string {
// This is like getStackByFiberInDevAndProd but just the first stack frame.
try {
const info = describeFiber(workTagMap, fiber, currentDispatcherRef);
if (info !== '') {
return info.slice(1); // skip the leading newline
}
} catch (x) {
console.error(x);
}
return null;
}
export function supportsConsoleTasks(fiber: Fiber): boolean {
// If this Fiber supports native console.createTask then we are already running
// inside a native async stack trace if it's active - meaning the DevTools is open.
File diff suppressed because it is too large Load Diff
@@ -907,30 +907,31 @@ export function attach(
}
}
function prepareViewAttributeSource(
function getElementAttributeByPath(
id: number,
path: Array<string | number>,
): void {
): mixed {
const inspectedElement = inspectElementRaw(id);
if (inspectedElement !== null) {
window.$attribute = getInObject(inspectedElement, path);
return getInObject(inspectedElement, path);
}
return undefined;
}
function prepareViewElementSource(id: number): void {
function getElementSourceFunctionById(id: number): null | Function {
const internalInstance = idToInternalInstanceMap.get(id);
if (internalInstance == null) {
console.warn(`Could not find instance with id "${id}"`);
return;
return null;
}
const element = internalInstance._currentElement;
if (element == null) {
console.warn(`Could not find element with id "${id}"`);
return;
return null;
}
global.$type = element.type;
return element.type;
}
function deletePath(
@@ -1141,8 +1142,8 @@ export function attach(
overrideValueAtPath,
renamePath,
patchConsoleForStrictMode,
prepareViewAttributeSource,
prepareViewElementSource,
getElementAttributeByPath,
getElementSourceFunctionById,
renderer,
setTraceUpdatesEnabled,
setTrackedPath,
+3 -3
View File
@@ -394,11 +394,11 @@ export type RendererInterface = {
value: any,
) => void,
patchConsoleForStrictMode: () => void,
prepareViewAttributeSource: (
getElementAttributeByPath: (
id: number,
path: Array<string | number>,
) => void,
prepareViewElementSource: (id: number) => void,
) => mixed,
getElementSourceFunctionById: (id: number) => null | Function,
renamePath: (
type: Type,
id: number,
@@ -93,7 +93,7 @@ export default function HoveredFiberInfo({fiberData}: Props): React.Node {
)}
<div className={styles.Content}>
{renderDurationInfo || <div>Did not render.</div>}
{renderDurationInfo || <div>Did not client render.</div>}
<WhatChanged fiberID={id} />
</div>
@@ -142,7 +142,7 @@ export default function SidebarSelectedFiberInfo(): React.Node {
</div>
)}
{listItems.length === 0 && (
<div>Did not render during this profiling session.</div>
<div>Did not render on the client during this profiling session.</div>
)}
</div>
</Fragment>
@@ -8677,4 +8677,65 @@ describe('ReactDOMFizzServer', () => {
'\n in Bar (at **)' + '\n in Foo (at **)',
);
});
it('can recover from very deep trees to avoid stack overflow', async () => {
function Recursive({n}) {
if (n > 0) {
return <Recursive n={n - 1} />;
}
return <span>hi</span>;
}
// Recursively render a component tree deep enough to trigger stack overflow.
// Don't make this too short to not hit the limit but also not too deep to slow
// down the test.
await act(() => {
const {pipe} = renderToPipeableStream(
<div>
<Recursive n={1000} />
</div>,
);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(
<div>
<span>hi</span>
</div>,
);
});
it('handles stack overflows inside components themselves', async () => {
function StackOverflow() {
// This component is recursive inside itself and is therefore an error.
// Assuming no tail-call optimizations.
function recursive(n, a0, a1, a2, a3) {
if (n > 0) {
return recursive(n - 1, a0, a1, a2, a3) + a0 + a1 + a2 + a3;
}
return a0;
}
return recursive(10000, 'should', 'not', 'resolve', 'this');
}
let caughtError;
await expect(async () => {
await act(() => {
const {pipe} = renderToPipeableStream(
<div>
<StackOverflow />
</div>,
{
onError(error, errorInfo) {
caughtError = error;
},
},
);
pipe(writable);
});
}).rejects.toThrow('Maximum call stack size exceeded');
expect(caughtError.message).toBe('Maximum call stack size exceeded');
});
});
@@ -46,7 +46,7 @@ describe('ReactSuspense', () => {
// Warning don't fire in production, so this test passes in prod even if
// the suspenseCallback feature is not enabled
// @gate www || !__DEV__
// @gate enableSuspenseCallback || !__DEV__
it('check type', async () => {
const {PromiseComp} = createThenable();
@@ -71,7 +71,7 @@ describe('ReactSuspense', () => {
await expect(async () => await waitForAll([])).toErrorDev([]);
});
// @gate www
// @gate enableSuspenseCallback
it('1 then 0 suspense callback', async () => {
const {promise, resolve, PromiseComp} = createThenable();
@@ -98,7 +98,7 @@ describe('ReactSuspense', () => {
expect(ops).toEqual([]);
});
// @gate www
// @gate enableSuspenseCallback
it('2 then 1 then 0 suspense callback', async () => {
const {
promise: promise1,
@@ -145,7 +145,7 @@ describe('ReactSuspense', () => {
expect(ops).toEqual([]);
});
// @gate www
// @gate enableSuspenseCallback
it('nested suspense promises are reported only for their tier', async () => {
const {promise, PromiseComp} = createThenable();
@@ -177,7 +177,7 @@ describe('ReactSuspense', () => {
expect(ops2).toEqual([new Set([promise])]);
});
// @gate www
// @gate enableSuspenseCallback
it('competing suspense promises', async () => {
const {
promise: promise1,
+76 -16
View File
@@ -3320,9 +3320,8 @@ function spawnNewSuspendedReplayTask(
request: Request,
task: ReplayTask,
thenableState: ThenableState | null,
x: Wakeable,
): void {
const newTask = createReplayTask(
): ReplayTask {
return createReplayTask(
request,
thenableState,
task.replay,
@@ -3340,17 +3339,13 @@ function spawnNewSuspendedReplayTask(
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
const ping = newTask.ping;
x.then(ping, ping);
}
function spawnNewSuspendedRenderTask(
request: Request,
task: RenderTask,
thenableState: ThenableState | null,
x: Wakeable,
): void {
): RenderTask {
// Something suspended, we'll need to create a new segment and resolve it later.
const segment = task.blockedSegment;
const insertionIndex = segment.chunks.length;
@@ -3367,7 +3362,7 @@ function spawnNewSuspendedRenderTask(
segment.children.push(newSegment);
// Reset lastPushedText for current Segment since the new Segment "consumed" it
segment.lastPushedText = false;
const newTask = createRenderTask(
return createRenderTask(
request,
thenableState,
task.node,
@@ -3385,9 +3380,6 @@ function spawnNewSuspendedRenderTask(
!disableLegacyContext ? task.legacyContext : emptyContextObject,
__DEV__ && enableOwnerStacks ? task.debugTask : null,
);
const ping = newTask.ping;
x.then(ping, ping);
}
// This is a non-destructive form of rendering a node. If it suspends it spawns
@@ -3436,13 +3428,47 @@ function renderNode(
if (typeof x.then === 'function') {
const wakeable: Wakeable = (x: any);
const thenableState = getThenableStateAfterSuspending();
spawnNewSuspendedReplayTask(
const newTask = spawnNewSuspendedReplayTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
wakeable,
);
const ping = newTask.ping;
wakeable.then(ping, ping);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
if (!disableLegacyContext) {
task.legacyContext = previousLegacyContext;
}
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
task.componentStack = previousComponentStack;
if (__DEV__ && enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
return;
}
if (x.message === 'Maximum call stack size exceeded') {
// This was a stack overflow. We do a lot of recursion in React by default for
// performance but it can lead to stack overflows in extremely deep trees.
// We do have the ability to create a trampoile if this happens which makes
// this kind of zero-cost.
const thenableState = getThenableStateAfterSuspending();
const newTask = spawnNewSuspendedReplayTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
);
// Immediately schedule the task for retrying.
request.pingedTasks.push(newTask);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
@@ -3493,13 +3519,14 @@ function renderNode(
if (typeof x.then === 'function') {
const wakeable: Wakeable = (x: any);
const thenableState = getThenableStateAfterSuspending();
spawnNewSuspendedRenderTask(
const newTask = spawnNewSuspendedRenderTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
wakeable,
);
const ping = newTask.ping;
wakeable.then(ping, ping);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
@@ -3540,6 +3567,39 @@ function renderNode(
);
trackPostpone(request, trackedPostpones, task, postponedSegment);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
if (!disableLegacyContext) {
task.legacyContext = previousLegacyContext;
}
task.context = previousContext;
task.keyPath = previousKeyPath;
task.treeContext = previousTreeContext;
task.componentStack = previousComponentStack;
if (__DEV__ && enableOwnerStacks) {
task.debugTask = previousDebugTask;
}
// Restore all active ReactContexts to what they were before.
switchContext(previousContext);
return;
}
if (x.message === 'Maximum call stack size exceeded') {
// This was a stack overflow. We do a lot of recursion in React by default for
// performance but it can lead to stack overflows in extremely deep trees.
// We do have the ability to create a trampoile if this happens which makes
// this kind of zero-cost.
const thenableState = getThenableStateAfterSuspending();
const newTask = spawnNewSuspendedRenderTask(
request,
// $FlowFixMe: Refined.
task,
thenableState,
);
// Immediately schedule the task for retrying.
request.pingedTasks.push(newTask);
// Restore the context. We assume that this will be restored by the inner
// functions in case nothing throws so we don't use "finally" here.
task.formatContext = previousFormatContext;
@@ -80,7 +80,7 @@ export const enableScopeAPI = false;
export const enableServerComponentLogs = true;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableSuspenseCallback = false;
export const enableSuspenseCallback = true;
export const enableTaint = true;
export const enableTransitionTracing = false;
export const enableTrustedTypesIntegration = false;