@samitouri / QOS-React / commits / a06cd9e1d1

[DevTools] Refactor Forcing Fallback / Error of Suspense / Error Boundaries (#30870)

First, this basically reverts https://github.com/facebook/react/pull/30517/commits/1f3892ef8cc181218587ddc6accd994890c92ef5 to use a Map/Set to track what is forced to suspend/error again instead of flags on the Instance. The difference is that now the key in the Fiber itself instead of the ID. Critically this avoids the fiberToFiberInstance map to look up whether or not a Fiber should be forced to suspend when asked by the renderer. This also allows us to force suspend/error on filtered instances. It's a bit unclear what should happen when you try to Suspend or Error a child but its parent boundary is filtered. It was also inconsistent between Suspense and Error due to how they were implemented. I think conceptually you're trying to simulate what would happen if that Component errored or suspended so it would be misleading if we triggered a different boundary than would happen in real life. So I think we should trigger the nearest unfiltered Fiber, not the nearest Instance. The consequence of this however is that if this instance was filtered, there's no way to undo it without refreshing or removing the filter. This is an edge case though since it's unusual you'd filter these in the first place. It used to be that Suspense walked the store in the frontend and Error walked the Fibers in the backend. They also did this somewhat eagerly. This simplifies and unifies the model by passing the id of what you clicked in the frontend and then we walk the Fiber tree from there in the backend to lazily find the boundary. However I also eagerly walk the tree at first to find whether we have any Suspense or Error boundary parents at all so we can hide the buttons if not. This also implements it to work with VirtualInstances using #30865. I find the nearest Fiber Instance downwards filtered or otherwise. Then from its parent we find the nearest Error or Suspense boundary. That's because VirtualInstance will always have their inner Fiber as an Instance but they might not have their parent since it might be filtered. Which would potentially cause us to skip over a filtered parent Suspense boundary.

Sebastian Markbåge committed Sep 5, 2024 at 15:48 UTC a06cd9e1d141f598a68377495f4c0fe9ee44e569
8 files changed +170 -257
packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
+1 -12
@@ -2975,16 +2975,12 @@ describe('InspectedElement', () => {
2975 // Inspect <ErrorBoundary /> and see that we cannot toggle error state
2976 // on error boundary itself
2977 let inspectedElement = await inspect(0);
2978 - expect(inspectedElement.canToggleError).toBe(false);
2979 - expect(inspectedElement.targetErrorBoundaryID).toBe(null);
2978 + expect(inspectedElement.canToggleError).toBe(true);
2979
2980 // Inspect <Example />
2981 inspectedElement = await inspect(1);
2982 expect(inspectedElement.canToggleError).toBe(true);
2983 expect(inspectedElement.isErrored).toBe(false);
2985 - expect(inspectedElement.targetErrorBoundaryID).toBe(
2986 - targetErrorBoundaryID,
2987 - );
2984
2985 // Suppress expected error and warning.
2986 const consoleErrorMock = jest
@@ -3009,10 +3005,6 @@ describe('InspectedElement', () => {
3005 inspectedElement = await inspect(0);
3006 expect(inspectedElement.canToggleError).toBe(true);
3007 expect(inspectedElement.isErrored).toBe(true);
3012 - // its error boundary ID is itself because it's caught the error
3013 - expect(inspectedElement.targetErrorBoundaryID).toBe(
3014 - targetErrorBoundaryID,
3015 - );
3008
3009 await toggleError(false);
3010
@@ -3020,9 +3012,6 @@ describe('InspectedElement', () => {
3012 inspectedElement = await inspect(1);
3013 expect(inspectedElement.canToggleError).toBe(true);
3014 expect(inspectedElement.isErrored).toBe(false);
3023 - expect(inspectedElement.targetErrorBoundaryID).toBe(
3024 - targetErrorBoundaryID,
3025 - );
3015 });
3016 });
3017
packages/react-devtools-shared/src/backend/fiber/renderer.js
+148 -133
@@ -148,11 +148,6 @@ const FIBER_INSTANCE = 0;
148 const VIRTUAL_INSTANCE = 1;
149 const FILTERED_FIBER_INSTANCE = 2;
150
151 -// Flags
152 -const FORCE_SUSPENSE_FALLBACK = /* */ 0b001;
153 -const FORCE_ERROR = /* */ 0b010;
154 -const FORCE_ERROR_RESET = /* */ 0b100;
155 -
151 // This type represents a stateful instance of a Client Component i.e. a Fiber pair.
152 // These instances also let us track stateful DevTools meta data like id and warnings.
153 type FiberInstance = {
@@ -161,7 +156,6 @@ type FiberInstance = {
156 parent: null | DevToolsInstance,
157 firstChild: null | DevToolsInstance,
158 nextSibling: null | DevToolsInstance,
164 - flags: number, // Force Error/Suspense
159 source: null | string | Error | Source, // source location of this component function, or owned child stack
160 errors: null | Map<string, number>, // error messages and count
161 warnings: null | Map<string, number>, // warning messages and count
@@ -176,7 +170,6 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
170 parent: null,
171 firstChild: null,
172 nextSibling: null,
179 - flags: 0,
173 source: null,
174 errors: null,
175 warnings: null,
@@ -193,7 +186,6 @@ type FilteredFiberInstance = {
186 parent: null | DevToolsInstance,
187 firstChild: null | DevToolsInstance,
188 nextSibling: null | DevToolsInstance,
196 - flags: number, // Force Error/Suspense
189 source: null | string | Error | Source, // always null here.
190 errors: null, // error messages and count
191 warnings: null, // warning messages and count
@@ -209,7 +201,6 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
201 parent: null,
202 firstChild: null,
203 nextSibling: null,
212 - flags: 0,
204 componentStack: null,
205 errors: null,
206 warnings: null,
@@ -229,7 +220,6 @@ type VirtualInstance = {
220 parent: null | DevToolsInstance,
221 firstChild: null | DevToolsInstance,
222 nextSibling: null | DevToolsInstance,
232 - flags: number,
223 source: null | string | Error | Source, // source location of this server component, or owned child stack
224 // Errors and Warnings happen per ReactComponentInfo which can appear in
225 // multiple places but we track them per stateful VirtualInstance so
@@ -251,7 +241,6 @@ function createVirtualInstance(
241 parent: null,
242 firstChild: null,
243 nextSibling: null,
254 - flags: 0,
244 source: null,
245 errors: null,
246 warnings: null,
@@ -1080,12 +1069,12 @@ export function attach(
1069 args: $ReadOnlyArray<any>,
1070 ): void {
1071 if (type === 'error') {
1083 - let fiberInstance = fiberToFiberInstanceMap.get(fiber);
1084 - if (fiberInstance === undefined && fiber.alternate !== null) {
1085 - fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
1086 - }
1072 // if this is an error simulated by us to trigger error boundary, ignore
1088 - if (fiberInstance !== undefined && fiberInstance.flags & FORCE_ERROR) {
1073 + if (
1074 + forceErrorForFibers.get(fiber) === true ||
1075 + (fiber.alternate !== null &&
1076 + forceErrorForFibers.get(fiber.alternate) === true)
1077 + ) {
1078 return;
1079 }
1080 }
@@ -1577,6 +1566,26 @@ export function attach(
1566 // Removes a Fiber (and its alternate) from the Maps used to track their id.
1567 // This method should always be called when a Fiber is unmounting.
1568 function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {
1569 + if (forceErrorForFibers.size > 0) {
1570 + forceErrorForFibers.delete(fiber);
1571 + if (fiber.alternate) {
1572 + forceErrorForFibers.delete(fiber.alternate);
1573 + }
1574 + if (forceErrorForFibers.size === 0 && setErrorHandler != null) {
1575 + setErrorHandler(shouldErrorFiberAlwaysNull);
1576 + }
1577 + }
1578 +
1579 + if (forceFallbackForFibers.size > 0) {
1580 + forceFallbackForFibers.delete(fiber);
1581 + if (fiber.alternate) {
1582 + forceFallbackForFibers.delete(fiber.alternate);
1583 + }
1584 + if (forceFallbackForFibers.size === 0 && setSuspenseHandler != null) {
1585 + setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1586 + }
1587 + }
1588 +
1589 // TODO: Consider using a WeakMap instead. The only thing where that doesn't work
1590 // is React Native Paper which tracks tags but that support is eventually going away
1591 // and can use the old findFiberByHostInstance strategy.
@@ -2465,21 +2474,6 @@ export function attach(
2474 fiberInstance.warnings = null;
2475 }
2476
2468 - if (fiberInstance.flags & FORCE_ERROR) {
2469 - fiberInstance.flags &= ~FORCE_ERROR;
2470 - forceErrorCount--;
2471 - if (forceErrorCount === 0 && setErrorHandler != null) {
2472 - setErrorHandler(shouldErrorFiberAlwaysNull);
2473 - }
2474 - }
2475 - if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) {
2476 - fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
2477 - forceFallbackCount--;
2478 - if (forceFallbackCount === 0 && setSuspenseHandler != null) {
2479 - setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
2480 - }
2481 - }
2482 -
2477 if (fiberToFiberInstanceMap.get(fiber) === fiberInstance) {
2478 fiberToFiberInstanceMap.delete(fiber);
2479 }
@@ -4208,18 +4202,6 @@ export function attach(
4202 }
4203 }
4204
4211 - function getNearestErrorBoundaryID(fiber: Fiber): number | null {
4212 - let parent = fiber.return;
4213 - while (parent !== null) {
4214 - if (isErrorBoundary(parent)) {
4215 - // TODO: If this boundary is filtered it won't have an ID.
4216 - return getFiberIDUnsafe(parent);
4217 - }
4218 - parent = parent.return;
4219 - }
4220 - return null;
4221 - }
4222 -
4205 function inspectElementRaw(id: number): InspectedElement | null {
4206 const devtoolsInstance = idToDevToolsInstanceMap.get(id);
4207 if (devtoolsInstance === undefined) {
@@ -4374,9 +4356,6 @@ export function attach(
4356 const owners: null | Array<SerializedElement> =
4357 getOwnersListFromInstance(fiberInstance);
4358
4377 - const isTimedOutSuspense =
4378 - tag === SuspenseComponent && memoizedState !== null;
4379 -
4359 let hooks = null;
4360 if (usesHooks) {
4361 const originalConsoleMethods: {[string]: $FlowFixMe} = {};
@@ -4406,16 +4385,26 @@ export function attach(
4385
4386 let rootType = null;
4387 let current = fiber;
4388 + let hasErrorBoundary = false;
4389 + let hasSuspenseBoundary = false;
4390 while (current.return !== null) {
4391 + const temp = current;
4392 current = current.return;
4393 + if (temp.tag === SuspenseComponent) {
4394 + hasSuspenseBoundary = true;
4395 + } else if (isErrorBoundary(temp)) {
4396 + hasErrorBoundary = true;
4397 + }
4398 }
4399 const fiberRoot = current.stateNode;
4400 if (fiberRoot != null && fiberRoot._debugRootType !== null) {
4401 rootType = fiberRoot._debugRootType;
4402 }
4403
4404 + const isTimedOutSuspense =
4405 + tag === SuspenseComponent && memoizedState !== null;
4406 +
4407 let isErrored = false;
4418 - let targetErrorBoundaryID;
4408 if (isErrorBoundary(fiber)) {
4409 // if the current inspected element is an error boundary,
4410 // either that we want to use it to toggle off error state
@@ -4428,12 +4417,9 @@ export function attach(
4417 const DidCapture = 0b000000000000000000010000000;
4418 isErrored =
4419 (fiber.flags & DidCapture) !== 0 ||
4431 - (fiberInstance.flags & FORCE_ERROR) !== 0;
4432 - targetErrorBoundaryID = isErrored
4433 - ? fiberInstance.id
4434 - : getNearestErrorBoundaryID(fiber);
4435 - } else {
4436 - targetErrorBoundaryID = getNearestErrorBoundaryID(fiber);
4420 + forceErrorForFibers.get(fiber) === true ||
4421 + (fiber.alternate !== null &&
4422 + forceErrorForFibers.get(fiber.alternate) === true);
4423 }
4424
4425 const plugins: Plugins = {
@@ -4468,18 +4454,20 @@ export function attach(
4454 canEditFunctionPropsRenamePaths:
4455 typeof overridePropsRenamePath === 'function',
4456
4471 - canToggleError: supportsTogglingError && targetErrorBoundaryID != null,
4457 + canToggleError: supportsTogglingError && hasErrorBoundary,
4458 // Is this error boundary in error state.
4459 isErrored,
4474 - targetErrorBoundaryID,
4460
4461 canToggleSuspense:
4462 supportsTogglingSuspense &&
4463 + hasSuspenseBoundary &&
4464 // If it's showing the real content, we can always flip fallback.
4465 (!isTimedOutSuspense ||
4466 // If it's showing fallback because we previously forced it to,
4467 // allow toggling it back to remove the fallback override.
4482 - (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0),
4468 + forceFallbackForFibers.has(fiber) ||
4469 + (fiber.alternate !== null &&
4470 + forceFallbackForFibers.has(fiber.alternate))),
4471
4472 // Can view component source location.
4473 canViewSource,
@@ -4533,22 +4521,24 @@ export function attach(
4521 getOwnersListFromInstance(virtualInstance);
4522
4523 let rootType = null;
4536 - let targetErrorBoundaryID = null;
4537 - let parent = virtualInstance.parent;
4538 - while (parent !== null) {
4539 - if (parent.kind !== VIRTUAL_INSTANCE) {
4540 - targetErrorBoundaryID = getNearestErrorBoundaryID(parent.data);
4541 - let current = parent.data;
4542 - while (current.return !== null) {
4543 - current = current.return;
4544 - }
4545 - const fiberRoot = current.stateNode;
4546 - if (fiberRoot != null && fiberRoot._debugRootType !== null) {
4547 - rootType = fiberRoot._debugRootType;
4524 + let hasErrorBoundary = false;
4525 + let hasSuspenseBoundary = false;
4526 + const nearestFiber = getNearestFiber(virtualInstance);
4527 + if (nearestFiber !== null) {
4528 + let current = nearestFiber;
4529 + while (current.return !== null) {
4530 + const temp = current;
4531 + current = current.return;
4532 + if (temp.tag === SuspenseComponent) {
4533 + hasSuspenseBoundary = true;
4534 + } else if (isErrorBoundary(temp)) {
4535 + hasErrorBoundary = true;
4536 }
4549 - break;
4537 }
4551 - parent = parent.parent;
4538 + const fiberRoot = current.stateNode;
4539 + if (fiberRoot != null && fiberRoot._debugRootType !== null) {
4540 + rootType = fiberRoot._debugRootType;
4541 + }
4542 }
4543
4544 const plugins: Plugins = {
@@ -4566,11 +4556,10 @@ export function attach(
4556 canEditFunctionPropsDeletePaths: false,
4557 canEditFunctionPropsRenamePaths: false,
4558
4569 - canToggleError: supportsTogglingError && targetErrorBoundaryID != null,
4559 + canToggleError: supportsTogglingError && hasErrorBoundary,
4560 isErrored: false,
4571 - targetErrorBoundaryID,
4561
4573 - canToggleSuspense: supportsTogglingSuspense,
4562 + canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary,
4563
4564 // Can view component source location.
4565 canViewSource,
@@ -5406,30 +5395,43 @@ export function attach(
5395 );
5396 }
5397
5398 + function getNearestFiber(devtoolsInstance: DevToolsInstance): null | Fiber {
5399 + if (devtoolsInstance.kind === VIRTUAL_INSTANCE) {
5400 + let inst: DevToolsInstance = devtoolsInstance;
5401 + while (inst.kind === VIRTUAL_INSTANCE) {
5402 + // For virtual instances, we search deeper until we find a Fiber instance.
5403 + // Then we search upwards from that Fiber. That's because Virtual Instances
5404 + // will always have an Fiber child filtered or not. If we searched its parents
5405 + // we might skip through a filtered Error Boundary before we hit a FiberInstance.
5406 + if (inst.firstChild === null) {
5407 + return null;
5408 + }
5409 + inst = inst.firstChild;
5410 + }
5411 + return inst.data.return;
5412 + } else {
5413 + return devtoolsInstance.data;
5414 + }
5415 + }
5416 +
5417 // React will switch between these implementations depending on whether
5418 // we have any manually suspended/errored-out Fibers or not.
5419 function shouldErrorFiberAlwaysNull() {
5420 return null;
5421 }
5422
5415 - let forceErrorCount = 0;
5423 + // Map of Fiber and its force error status: true (error), false (toggled off)
5424 + const forceErrorForFibers = new Map<Fiber, boolean>();
5425
5417 - function shouldErrorFiberAccordingToMap(fiber: any): null | boolean {
5426 + function shouldErrorFiberAccordingToMap(fiber: any): boolean {
5427 if (typeof setErrorHandler !== 'function') {
5428 throw new Error(
5429 'Expected overrideError() to not get called for earlier React versions.',
5430 );
5431 }
5432
5424 - let fiberInstance = fiberToFiberInstanceMap.get(fiber);
5425 - if (fiberInstance === undefined && fiber.alternate !== null) {
5426 - fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
5427 - }
5428 - if (fiberInstance === undefined) {
5429 - return null;
5430 - }
5431 -
5432 - if (fiberInstance.flags & FORCE_ERROR_RESET) {
5433 + let status = forceErrorForFibers.get(fiber);
5434 + if (status === false) {
5435 // TRICKY overrideError adds entries to this Map,
5436 // so ideally it would be the method that clears them too,
5437 // but that would break the functionality of the feature,
@@ -5439,18 +5441,27 @@ export function attach(
5441 // Technically this is premature and we should schedule it for later,
5442 // since the render could always fail without committing the updated error boundary,
5443 // but since this is a DEV-only feature, the simplicity is worth the trade off.
5442 - forceErrorCount--;
5443 - fiberInstance.flags &= ~FORCE_ERROR_RESET;
5444 - if (forceErrorCount === 0) {
5444 + forceErrorForFibers.delete(fiber);
5445 + if (forceErrorForFibers.size === 0) {
5446 // Last override is gone. Switch React back to fast path.
5447 setErrorHandler(shouldErrorFiberAlwaysNull);
5448 }
5449 return false;
5449 - } else if (fiberInstance.flags & FORCE_ERROR) {
5450 - return true;
5451 - } else {
5452 - return null;
5450 }
5451 + if (status === undefined && fiber.alternate !== null) {
5452 + status = forceErrorForFibers.get(fiber.alternate);
5453 + if (status === false) {
5454 + forceErrorForFibers.delete(fiber.alternate);
5455 + if (forceErrorForFibers.size === 0) {
5456 + // Last override is gone. Switch React back to fast path.
5457 + setErrorHandler(shouldErrorFiberAlwaysNull);
5458 + }
5459 + }
5460 + }
5461 + if (status === undefined) {
5462 + return false;
5463 + }
5464 + return status;
5465 }
5466
5467 function overrideError(id: number, forceError: boolean) {
@@ -5467,38 +5478,39 @@ export function attach(
5478 if (devtoolsInstance === undefined) {
5479 return;
5480 }
5470 - if ((devtoolsInstance.flags & (FORCE_ERROR | FORCE_ERROR_RESET)) === 0) {
5471 - forceErrorCount++;
5472 - if (forceErrorCount === 1) {
5473 - // First override is added. Switch React to slower path.
5474 - setErrorHandler(shouldErrorFiberAccordingToMap);
5481 + const nearestFiber = getNearestFiber(devtoolsInstance);
5482 + if (nearestFiber === null) {
5483 + return;
5484 + }
5485 + let fiber = nearestFiber;
5486 + while (!isErrorBoundary(fiber)) {
5487 + if (fiber.return === null) {
5488 + return;
5489 }
5490 + fiber = fiber.return;
5491 }
5477 - devtoolsInstance.flags &= forceError ? ~FORCE_ERROR_RESET : ~FORCE_ERROR;
5478 - devtoolsInstance.flags |= forceError ? FORCE_ERROR : FORCE_ERROR_RESET;
5479 -
5480 - if (devtoolsInstance.kind === FIBER_INSTANCE) {
5481 - const fiber = devtoolsInstance.data;
5482 - scheduleUpdate(fiber);
5483 - } else {
5484 - // TODO: Handle VirtualInstance.
5492 + forceErrorForFibers.set(fiber, forceError);
5493 + if (fiber.alternate !== null) {
5494 + // We only need one of the Fibers in the set.
5495 + forceErrorForFibers.delete(fiber.alternate);
5496 + }
5497 + if (forceErrorForFibers.size === 1) {
5498 + // First override is added. Switch React to slower path.
5499 + setErrorHandler(shouldErrorFiberAccordingToMap);
5500 }
5501 + scheduleUpdate(fiber);
5502 }
5503
5504 function shouldSuspendFiberAlwaysFalse() {
5505 return false;
5506 }
5507
5492 - let forceFallbackCount = 0;
5508 + const forceFallbackForFibers = new Set<Fiber>();
5509
5494 - function shouldSuspendFiberAccordingToSet(fiber: any) {
5495 - let fiberInstance = fiberToFiberInstanceMap.get(fiber);
5496 - if (fiberInstance === undefined && fiber.alternate !== null) {
5497 - fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate);
5498 - }
5510 + function shouldSuspendFiberAccordingToSet(fiber: Fiber): boolean {
5511 return (
5500 - fiberInstance !== undefined &&
5501 - (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0
5512 + forceFallbackForFibers.has(fiber) ||
5513 + (fiber.alternate !== null && forceFallbackForFibers.has(fiber.alternate))
5514 );
5515 }
5516
@@ -5515,33 +5527,36 @@ export function attach(
5527 if (devtoolsInstance === undefined) {
5528 return;
5529 }
5530 + const nearestFiber = getNearestFiber(devtoolsInstance);
5531 + if (nearestFiber === null) {
5532 + return;
5533 + }
5534 + let fiber = nearestFiber;
5535 + while (fiber.tag !== SuspenseComponent) {
5536 + if (fiber.return === null) {
5537 + return;
5538 + }
5539 + fiber = fiber.return;
5540 + }
5541
5542 + if (fiber.alternate !== null) {
5543 + // We only need one of the Fibers in the set.
5544 + forceFallbackForFibers.delete(fiber.alternate);
5545 + }
5546 if (forceFallback) {
5520 - if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) === 0) {
5521 - devtoolsInstance.flags |= FORCE_SUSPENSE_FALLBACK;
5522 - forceFallbackCount++;
5523 - if (forceFallbackCount === 1) {
5524 - // First override is added. Switch React to slower path.
5525 - setSuspenseHandler(shouldSuspendFiberAccordingToSet);
5526 - }
5547 + forceFallbackForFibers.add(fiber);
5548 + if (forceFallbackForFibers.size === 1) {
5549 + // First override is added. Switch React to slower path.
5550 + setSuspenseHandler(shouldSuspendFiberAccordingToSet);
5551 }
5552 } else {
5529 - if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0) {
5530 - devtoolsInstance.flags &= ~FORCE_SUSPENSE_FALLBACK;
5531 - forceFallbackCount--;
5532 - if (forceFallbackCount === 0) {
5533 - // Last override is gone. Switch React back to fast path.
5534 - setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
5535 - }
5553 + forceFallbackForFibers.delete(fiber);
5554 + if (forceFallbackForFibers.size === 0) {
5555 + // Last override is gone. Switch React back to fast path.
5556 + setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
5557 }
5558 }
5538 -
5539 - if (devtoolsInstance.kind === FIBER_INSTANCE) {
5540 - const fiber = devtoolsInstance.data;
5541 - scheduleUpdate(fiber);
5542 - } else {
5543 - // TODO: Handle VirtualInstance.
5544 - }
5559 + scheduleUpdate(fiber);
5560 }
5561
5562 // Remember if we're trying to restore the selection after reload.
packages/react-devtools-shared/src/backend/legacy/renderer.js
-1
@@ -826,7 +826,6 @@ export function attach(
826 // Toggle error boundary did not exist in legacy versions
827 canToggleError: false,
828 isErrored: false,
829 - targetErrorBoundaryID: null,
829
830 // Suspense did not exist in legacy versions
831 canToggleSuspense: false,
packages/react-devtools-shared/src/backend/types.js
-1
@@ -252,7 +252,6 @@ export type InspectedElement = {
252 // Is this Error, and can its value be overridden now?
253 canToggleError: boolean,
254 isErrored: boolean,
255 - targetErrorBoundaryID: ?number,
255
256 // Is this Suspense, and can its value be overridden now?
257 canToggleSuspense: boolean,
packages/react-devtools-shared/src/backendAPI.js
-2
@@ -221,7 +221,6 @@ export function convertInspectedElementBackendToFrontend(
221 canEditHooksAndRenamePaths,
222 canToggleError,
223 isErrored,
224 - targetErrorBoundaryID,
224 canToggleSuspense,
225 canViewSource,
226 hasLegacyContext,
@@ -251,7 +250,6 @@ export function convertInspectedElementBackendToFrontend(
250 canEditHooksAndRenamePaths,
251 canToggleError,
252 isErrored,
254 - targetErrorBoundaryID,
253 canToggleSuspense,
254 canViewSource,
255 hasLegacyContext,
packages/react-devtools-shared/src/devtools/views/Components/CannotSuspendWarningMessage.js deleted
-44
@@ -1,44 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import * as React from 'react';
11 -import {useContext} from 'react';
12 -import {StoreContext} from '../context';
13 -import {
14 - ComponentFilterElementType,
15 - ElementTypeSuspense,
16 -} from 'react-devtools-shared/src/frontend/types';
17 -
18 -export default function CannotSuspendWarningMessage(): React.Node {
19 - const store = useContext(StoreContext);
20 - const areSuspenseElementsHidden = !!store.componentFilters.find(
21 - filter =>
22 - filter.type === ComponentFilterElementType &&
23 - filter.value === ElementTypeSuspense &&
24 - filter.isEnabled,
25 - );
26 -
27 - // Has the user filtered out Suspense nodes from the tree?
28 - // If so, the selected element might actually be in a Suspense tree after all.
29 - if (areSuspenseElementsHidden) {
30 - return (
31 - <div>
32 - Suspended state cannot be toggled while Suspense components are hidden.
33 - Disable the filter and try again.
34 - </div>
35 - );
36 - } else {
37 - return (
38 - <div>
39 - The selected element is not within a Suspense container. Suspending it
40 - would cause an error.
41 - </div>
42 - );
43 - }
44 -}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+21 -63
@@ -9,15 +9,13 @@
9
10 import * as React from 'react';
11 import {useCallback, useContext, useSyncExternalStore} from 'react';
12 -import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
12 +import {TreeStateContext} from './TreeContext';
13 import {BridgeContext, StoreContext, OptionsContext} from '../context';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 import Icon from '../Icon';
17 -import {ModalDialogContext} from '../ModalDialog';
17 import Toggle from '../Toggle';
18 import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
20 -import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
19 import InspectedElementView from './InspectedElementView';
20 import {InspectedElementContext} from './InspectedElementContext';
21 import {getOpenInEditorURL} from '../../../utils';
@@ -38,7 +36,6 @@ export type Props = {};
36
37 export default function InspectedElementWrapper(_: Props): React.Node {
38 const {inspectedElementID} = useContext(TreeStateContext);
41 - const dispatch = useContext(TreeDispatcherContext);
39 const bridge = useContext(BridgeContext);
40 const store = useContext(StoreContext);
41 const {
@@ -47,7 +44,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
44 hideLogAction,
45 hideViewSourceAction,
46 } = useContext(OptionsContext);
50 - const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
47
48 const {hookNames, inspectedElement, parseHookNames, toggleParseHookNames} =
49 useContext(InspectedElementContext);
@@ -105,8 +101,6 @@ export default function InspectedElementWrapper(_: Props): React.Node {
101 }, [bridge, inspectedElementID, store]);
102
103 const isErrored = inspectedElement != null && inspectedElement.isErrored;
108 - const targetErrorBoundaryID =
109 - inspectedElement != null ? inspectedElement.targetErrorBoundaryID : null;
104
105 const isSuspended =
106 element !== null &&
@@ -137,79 +131,43 @@ export default function InspectedElementWrapper(_: Props): React.Node {
131 );
132
133 const toggleErrored = useCallback(() => {
140 - if (inspectedElement == null || targetErrorBoundaryID == null) {
134 + if (inspectedElement == null) {
135 return;
136 }
137
144 - const rendererID = store.getRendererIDForElement(targetErrorBoundaryID);
138 + const rendererID = store.getRendererIDForElement(inspectedElement.id);
139 if (rendererID !== null) {
146 - if (targetErrorBoundaryID !== inspectedElement.id) {
147 - // Update tree selection so that if we cause a component to error,
148 - // the nearest error boundary will become the newly selected thing.
149 - dispatch({
150 - type: 'SELECT_ELEMENT_BY_ID',
151 - payload: targetErrorBoundaryID,
152 - });
153 - }
154 -
140 // Toggle error.
141 + // Because triggering an error will always delete the children, we'll
142 + // automatically select the nearest still mounted instance which will be
143 + // the error boundary.
144 bridge.send('overrideError', {
157 - id: targetErrorBoundaryID,
145 + id: inspectedElement.id,
146 rendererID,
147 forceError: !isErrored,
148 });
149 }
162 - }, [bridge, dispatch, isErrored, targetErrorBoundaryID]);
150 + }, [bridge, store, isErrored, inspectedElement]);
151
152 // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
153 const toggleSuspended = useCallback(() => {
166 - let nearestSuspenseElement = null;
167 - let currentElement = element;
168 - while (currentElement !== null) {
169 - if (currentElement.type === ElementTypeSuspense) {
170 - nearestSuspenseElement = currentElement;
171 - break;
172 - } else if (currentElement.parentID > 0) {
173 - currentElement = store.getElementByID(currentElement.parentID);
174 - } else {
175 - currentElement = null;
176 - }
154 + if (inspectedElement == null) {
155 + return;
156 }
157
179 - // If we didn't find a Suspense ancestor, we can't suspend.
180 - // Instead we can show a warning to the user.
181 - if (nearestSuspenseElement === null) {
182 - modalDialogDispatch({
183 - id: 'InspectedElement',
184 - type: 'SHOW',
185 - content: <CannotSuspendWarningMessage />,
186 - });
187 - } else {
188 - const nearestSuspenseElementID = nearestSuspenseElement.id;
189 -
190 - // If we're suspending from an arbitrary (non-Suspense) component, select the nearest Suspense element in the Tree.
191 - // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
192 - if (nearestSuspenseElement !== element) {
193 - dispatch({
194 - type: 'SELECT_ELEMENT_BY_ID',
195 - payload: nearestSuspenseElementID,
196 - });
197 - }
198 -
199 - const rendererID = store.getRendererIDForElement(
200 - nearestSuspenseElementID,
201 - );
202 -
158 + const rendererID = store.getRendererIDForElement(inspectedElement.id);
159 + if (rendererID !== null) {
160 // Toggle suspended
204 - if (rendererID !== null) {
205 - bridge.send('overrideSuspense', {
206 - id: nearestSuspenseElementID,
207 - rendererID,
208 - forceFallback: !isSuspended,
209 - });
210 - }
161 + // Because suspending or unsuspending always delete the children or fallback,
162 + // we'll automatically select the nearest still mounted instance which will be
163 + // the Suspense boundary.
164 + bridge.send('overrideSuspense', {
165 + id: inspectedElement.id,
166 + rendererID,
167 + forceFallback: !isSuspended,
168 + });
169 }
212 - }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
170 + }, [bridge, store, isSuspended, inspectedElement]);
171
172 if (element === null) {
173 return (
packages/react-devtools-shared/src/frontend/types.js
-1
@@ -219,7 +219,6 @@ export type InspectedElement = {
219 // Is this Error, and can its value be overridden now?
220 isErrored: boolean,
221 canToggleError: boolean,
222 - targetErrorBoundaryID: ?number,
222
223 // Is this Suspense, and can its value be overridden now?
224 canToggleSuspense: boolean,