mirror of
https://github.com/hanzoai/react.git
synced 2026-09-24 17:02:27 +00:00
[enableInfiniteRenderLoopDetection] Add a flag to force throwing (#36357)
Adds a `enableInfiniteRenderLoopDetectionForceThrow` flag, which changes the detection mechanism behaviour to start throwing, when the loop is observed. By default, the value is set to `false`, also made dynamic from the start for `www`.
This commit is contained in:
+151
-3
@@ -1793,7 +1793,14 @@ describe('ReactUpdates', () => {
|
||||
});
|
||||
|
||||
it("warns about potential infinite loop if there's a synchronous render phase update on another component", async () => {
|
||||
if (!__DEV__ || gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
|
||||
if (
|
||||
!__DEV__ ||
|
||||
gate(
|
||||
flags =>
|
||||
!flags.enableInfiniteRenderLoopDetection ||
|
||||
flags.enableInfiniteRenderLoopDetectionForceThrow,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let setState;
|
||||
@@ -1831,7 +1838,14 @@ describe('ReactUpdates', () => {
|
||||
});
|
||||
|
||||
it("warns about potential infinite loop if there's an async render phase update on another component", async () => {
|
||||
if (!__DEV__ || gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
|
||||
if (
|
||||
!__DEV__ ||
|
||||
gate(
|
||||
flags =>
|
||||
!flags.enableInfiniteRenderLoopDetection ||
|
||||
flags.enableInfiniteRenderLoopDetectionForceThrow,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let setState;
|
||||
@@ -2007,7 +2021,14 @@ describe('ReactUpdates', () => {
|
||||
});
|
||||
|
||||
it('warns instead of throwing when infinite Suspense ping loop is detected via enableInfiniteRenderLoopDetection during commit phase', async () => {
|
||||
if (!__DEV__ || gate(flags => !flags.enableInfiniteRenderLoopDetection)) {
|
||||
if (
|
||||
!__DEV__ ||
|
||||
gate(
|
||||
flags =>
|
||||
!flags.enableInfiniteRenderLoopDetection ||
|
||||
flags.enableInfiniteRenderLoopDetectionForceThrow,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2115,6 +2136,133 @@ describe('ReactUpdates', () => {
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
// @gate enableInfiniteRenderLoopDetection && enableInfiniteRenderLoopDetectionForceThrow
|
||||
it('throws when sync render-phase update loop is detected with force-throw enabled', async () => {
|
||||
// Render-phase setState on another component's hook produces a sync
|
||||
// recursive update. With ForceThrow enabled this should throw via
|
||||
// throwForcedInfiniteRenderLoopError instead of only warning in DEV.
|
||||
let setState;
|
||||
let shouldStop = false;
|
||||
function App() {
|
||||
const [, _setState] = React.useState(0);
|
||||
setState = _setState;
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
function Child() {
|
||||
if (shouldStop) {
|
||||
return null;
|
||||
}
|
||||
setState(n => n + 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const errors = [];
|
||||
const captureError = error => {
|
||||
errors.push(error.message);
|
||||
// Stop scheduling new updates so the test (and the gate-off variant
|
||||
// where the legacy error path is recoverable) can terminate cleanly
|
||||
// without tripping the babel infinite-loop guard.
|
||||
shouldStop = true;
|
||||
};
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError: captureError,
|
||||
onRecoverableError: captureError,
|
||||
onCaughtError: captureError,
|
||||
});
|
||||
|
||||
// The render-phase setState path also produces a dev-only "Cannot update
|
||||
// a component while rendering a different component" console.error on
|
||||
// every recursion. Swallow those so the test framework doesn't require
|
||||
// us to assert each one.
|
||||
const originalConsoleError = console.error;
|
||||
console.error = msg => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.startsWith('Cannot update a component')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleError(msg);
|
||||
};
|
||||
try {
|
||||
await act(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
expect(errors.length).toBeGreaterThanOrEqual(1);
|
||||
expect(errors[0]).toContain(
|
||||
'Maximum update depth exceeded. This could be an infinite loop.',
|
||||
);
|
||||
});
|
||||
|
||||
// @gate enableInfiniteRenderLoopDetection && enableInfiniteRenderLoopDetectionForceThrow
|
||||
it('throws when phase-spawn update loop is detected with force-throw enabled', async () => {
|
||||
// Wrapping the initial render in startTransition makes the render-phase
|
||||
// setState inherit a non-sync transition lane. After commit, the next
|
||||
// render is non-sync, so the loop detector classifies the recursion as
|
||||
// NESTED_UPDATE_PHASE_SPAWN (rather than SYNC_LANE). With ForceThrow
|
||||
// enabled, this branch should throw via throwForcedInfiniteRenderLoopError
|
||||
// instead of only warning in DEV.
|
||||
let setState;
|
||||
let shouldStop = false;
|
||||
// Hard cap on Child renders. Without enableInfiniteRenderLoopDetection,
|
||||
// the PHASE_SPAWN branch is gated off entirely, so no throw fires and
|
||||
// the loop would otherwise run until the babel infinite-loop guard.
|
||||
let renderCount = 0;
|
||||
const RENDER_CAP = 100;
|
||||
function App() {
|
||||
const [, _setState] = React.useState(0);
|
||||
setState = _setState;
|
||||
return <Child />;
|
||||
}
|
||||
|
||||
function Child() {
|
||||
if (shouldStop || renderCount >= RENDER_CAP) {
|
||||
return null;
|
||||
}
|
||||
renderCount++;
|
||||
setState(n => n + 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
const errors = [];
|
||||
const root = ReactDOMClient.createRoot(container, {
|
||||
onUncaughtError: error => {
|
||||
errors.push(error.message);
|
||||
shouldStop = true;
|
||||
},
|
||||
});
|
||||
|
||||
const originalConsoleError = console.error;
|
||||
console.error = msg => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
msg.startsWith('Cannot update a component')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
originalConsoleError(msg);
|
||||
};
|
||||
try {
|
||||
await act(() => {
|
||||
React.startTransition(() => root.render(<App />));
|
||||
});
|
||||
} finally {
|
||||
console.error = originalConsoleError;
|
||||
}
|
||||
|
||||
expect(errors.length).toBeGreaterThanOrEqual(1);
|
||||
expect(errors[0]).toContain(
|
||||
'Maximum update depth exceeded. This could be an infinite loop.',
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents infinite update loop triggered by too many updates in ref callbacks', async () => {
|
||||
let scheduleUpdate;
|
||||
function TooManyRefUpdates() {
|
||||
|
||||
+37
-4
@@ -51,6 +51,7 @@ import {
|
||||
disableLegacyContext,
|
||||
alwaysThrottleRetries,
|
||||
enableInfiniteRenderLoopDetection,
|
||||
enableInfiniteRenderLoopDetectionForceThrow,
|
||||
disableLegacyMode,
|
||||
enableComponentPerformanceTrack,
|
||||
enableYieldingBeforePassive,
|
||||
@@ -5175,6 +5176,27 @@ export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
|
||||
retryTimedOutBoundary(boundaryFiber, retryLane);
|
||||
}
|
||||
|
||||
function throwForcedInfiniteRenderLoopError(
|
||||
root: FiberRoot | null,
|
||||
renderLanes: Lanes,
|
||||
): empty {
|
||||
if (root !== null) {
|
||||
// Disable concurrent error recovery for the in-progress render so the thrown
|
||||
// error reaches the nearest error boundary and breaks the infinite update
|
||||
// loop instead of being silently retried by the recovery mechanism.
|
||||
root.errorRecoveryDisabledLanes = mergeLanes(
|
||||
root.errorRecoveryDisabledLanes,
|
||||
renderLanes,
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
|
||||
'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
|
||||
'causing infinite render loop. React limits the number of nested updates to ' +
|
||||
'prevent infinite loops.',
|
||||
);
|
||||
}
|
||||
|
||||
export function throwIfInfiniteUpdateLoopDetected(
|
||||
isFromInfiniteRenderLoopDetectionInstrumentation: boolean,
|
||||
) {
|
||||
@@ -5191,10 +5213,16 @@ export function throwIfInfiniteUpdateLoopDetected(
|
||||
if (updateKind === NESTED_UPDATE_SYNC_LANE) {
|
||||
if (
|
||||
isFromInfiniteRenderLoopDetectionInstrumentation ||
|
||||
(executionContext & RenderContext && workInProgressRoot !== null)
|
||||
(executionContext & RenderContext) !== NoContext
|
||||
) {
|
||||
// This loop was identified only because of the instrumentation gated with enableInfiniteRenderLoopDetection, warn instead of throwing.
|
||||
if (__DEV__) {
|
||||
// This loop was identified only because of the instrumentation gated with enableInfiniteRenderLoopDetection,
|
||||
// warn instead of throwing, unless enableInfiniteRenderLoopDetectionForceThrow.
|
||||
if (enableInfiniteRenderLoopDetectionForceThrow) {
|
||||
throwForcedInfiniteRenderLoopError(
|
||||
workInProgressRoot,
|
||||
workInProgressRootRenderLanes,
|
||||
);
|
||||
} else if (__DEV__) {
|
||||
console.error(
|
||||
'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
|
||||
'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
|
||||
@@ -5211,7 +5239,12 @@ export function throwIfInfiniteUpdateLoopDetected(
|
||||
);
|
||||
}
|
||||
} else if (updateKind === NESTED_UPDATE_PHASE_SPAWN) {
|
||||
if (__DEV__) {
|
||||
if (enableInfiniteRenderLoopDetectionForceThrow) {
|
||||
throwForcedInfiniteRenderLoopError(
|
||||
workInProgressRoot,
|
||||
workInProgressRootRenderLanes,
|
||||
);
|
||||
} else if (__DEV__) {
|
||||
console.error(
|
||||
'Maximum update depth exceeded. This could be an infinite loop. This can happen when a component ' +
|
||||
'repeatedly calls setState during render phase or inside useLayoutEffect, ' +
|
||||
|
||||
@@ -144,6 +144,12 @@ export const transitionLaneExpirationMs = 5000;
|
||||
* by setState or similar outside of the component owning the state.
|
||||
*/
|
||||
export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
/**
|
||||
* When `enableInfiniteRenderLoopDetection` is on, forces the detection
|
||||
* mechanism to throw instead of only warning in cases where it would
|
||||
* otherwise downgrade to a warning.
|
||||
*/
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = false;
|
||||
|
||||
export const enableFragmentRefs: boolean = true;
|
||||
export const enableFragmentRefsScrollIntoView: boolean = true;
|
||||
|
||||
@@ -47,6 +47,7 @@ export const enableCreateEventHandleAPI: boolean = false;
|
||||
export const enableMoveBefore: boolean = true;
|
||||
export const enableFizzExternalRuntime: boolean = true;
|
||||
export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = false;
|
||||
export const enableLegacyCache: boolean = false;
|
||||
export const enableLegacyFBSupport: boolean = false;
|
||||
export const enableLegacyHidden: boolean = false;
|
||||
|
||||
@@ -31,6 +31,7 @@ export const enableCreateEventHandleAPI: boolean = false;
|
||||
export const enableMoveBefore: boolean = true;
|
||||
export const enableFizzExternalRuntime: boolean = true;
|
||||
export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = false;
|
||||
export const enableLegacyCache: boolean = false;
|
||||
export const enableLegacyFBSupport: boolean = false;
|
||||
export const enableLegacyHidden: boolean = false;
|
||||
|
||||
@@ -52,6 +52,7 @@ export const passChildrenWhenCloningPersistedNodes: boolean = false;
|
||||
export const disableClientCache: boolean = true;
|
||||
|
||||
export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = false;
|
||||
|
||||
export const enableEagerAlternateStateNodeCleanup: boolean = true;
|
||||
export const enableEffectEventMutationPhase: boolean = false;
|
||||
|
||||
@@ -26,6 +26,7 @@ export const enableCreateEventHandleAPI = false;
|
||||
export const enableMoveBefore = false;
|
||||
export const enableFizzExternalRuntime = true;
|
||||
export const enableInfiniteRenderLoopDetection = false;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow = false;
|
||||
export const enableLegacyCache = false;
|
||||
export const enableLegacyFBSupport = false;
|
||||
export const enableLegacyHidden = false;
|
||||
|
||||
@@ -54,6 +54,7 @@ export const passChildrenWhenCloningPersistedNodes: boolean = false;
|
||||
export const disableClientCache: boolean = true;
|
||||
|
||||
export const enableInfiniteRenderLoopDetection: boolean = false;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = false;
|
||||
|
||||
export const enableReactTestRendererWarning: boolean = false;
|
||||
export const disableLegacyMode: boolean = true;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const transitionLaneExpirationMs = 5000;
|
||||
export const enableSchedulingProfiler: boolean = __VARIANT__;
|
||||
|
||||
export const enableInfiniteRenderLoopDetection: boolean = __VARIANT__;
|
||||
export const enableInfiniteRenderLoopDetectionForceThrow: boolean = __VARIANT__;
|
||||
|
||||
export const enableFastAddPropertiesInDiffing: boolean = __VARIANT__;
|
||||
export const enableSuspenseyImages: boolean = __VARIANT__;
|
||||
|
||||
@@ -20,6 +20,7 @@ export const {
|
||||
disableSchedulerTimeoutInWorkLoop,
|
||||
enableEffectEventMutationPhase,
|
||||
enableInfiniteRenderLoopDetection,
|
||||
enableInfiniteRenderLoopDetectionForceThrow,
|
||||
enableNoCloningMemoCache,
|
||||
enableObjectFiber,
|
||||
enableRetryLaneExpiration,
|
||||
|
||||
@@ -582,5 +582,6 @@
|
||||
"594": "Cannot read Symbol exports. Only named exports are supported on a client module imported on the server.",
|
||||
"595": "Attempted to call %s() from the server but %s is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.",
|
||||
"596": "Could not find the module \"%s\" in the React Client Manifest. This is probably a bug in the React Server Components bundler.",
|
||||
"597": "The module \"%s\" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler."
|
||||
"597": "The module \"%s\" is marked as an async ESM module but was loaded as a CJS proxy. This is probably a bug in the React Server Components bundler.",
|
||||
"598": "Maximum update depth exceeded. This could be an infinite loop. This can happen when a component repeatedly calls setState during render phase or inside useLayoutEffect, causing infinite render loop. React limits the number of nested updates to prevent infinite loops."
|
||||
}
|
||||
Reference in New Issue
Block a user