diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 0b48effe1902d..8ba5e6503651d 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -7,6 +7,8 @@ * @flow */ +import type {ReactComponentInfo} from 'shared/ReactTypes'; + import { ComponentFilterDisplayName, ComponentFilterElementType, @@ -131,6 +133,64 @@ import type { import type {Source} from 'react-devtools-shared/src/shared/types'; import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack'; +// Kinds +const FIBER_INSTANCE = 0; +// const VIRTUAL_INSTANCE = 1; + +// Flags +const FORCE_SUSPENSE_FALLBACK = /* */ 0b001; +const FORCE_ERROR = /* */ 0b010; +const FORCE_ERROR_RESET = /* */ 0b100; + +// This type represents a stateful instance of a Client Component i.e. a Fiber pair. +// These instances also let us track stateful DevTools meta data like id and warnings. +type FiberInstance = { + kind: 0, + id: number, + parent: null | DevToolsInstance, // virtual parent + flags: number, // Force Error/Suspense + componentStack: null | string, + errors: null | Map, // error messages and count + warnings: null | Map, // warning messages and count + data: Fiber, // one of a Fiber pair +}; + +function createFiberInstance(fiber: Fiber): FiberInstance { + return { + kind: 0, + id: getUID(), + parent: null, + flags: 0, + componentStack: null, + errors: null, + warnings: null, + data: fiber, + }; +} + +// This type represents a stateful instance of a Server Component or a Component +// that gets optimized away - e.g. call-through without creating a Fiber. +// It's basically a virtual Fiber. This is not a semantic concept in React. +// It only exists as a virtual concept to let the same Element in the DevTools +// persist. To be selectable separately from all ReactComponentInfo and overtime. +type VirtualInstance = { + kind: 1, + id: number, + parent: null | DevToolsInstance, // virtual parent + flags: number, + componentStack: null | string, + // Errors and Warnings happen per ReactComponentInfo which can appear in + // multiple places but we track them per stateful VirtualInstance so + // that old errors/warnings don't disappear when the instance is refreshed. + errors: null | Map, // error messages and count + warnings: null | Map, // warning messages and count + // The latest info for this instance. This can be updated over time and the + // same info can appear in more than once ServerComponentInstance. + data: ReactComponentInfo, +}; + +type DevToolsInstance = FiberInstance | VirtualInstance; + type getDisplayNameForFiberType = (fiber: Fiber) => string | null; type getTypeSymbolType = (type: any) => symbol | number; @@ -629,14 +689,12 @@ export function getInternalReactConstants(version: string): { // We track both Fibers to support Fast Refresh, // which may forcefully replace one of the pair as part of hot reloading. // In that case it's still important to be able to locate the previous ID during subsequent renders. -const fiberToIDMap: Map = new Map(); +const fiberToFiberInstanceMap: Map = new Map(); // Map of id to one (arbitrary) Fiber in a pair. // This Map is used to e.g. get the display name for a Fiber or schedule an update, // operations that should be the same whether the current and work-in-progress Fiber is used. -const idToArbitraryFiberMap: Map = new Map(); - -const fiberToComponentStackMap: WeakMap = new WeakMap(); +const idToDevToolsInstanceMap: Map = new Map(); export function attach( hook: DevToolsHook, @@ -750,81 +808,80 @@ export function attach( } // Tracks Fibers with recently changed number of error/warning messages. - // These collections store the Fiber rather than the ID, - // in order to avoid generating an ID for Fibers that never get mounted + // These collections store the Fiber rather than the DevToolsInstance, + // in order to avoid generating an DevToolsInstance for Fibers that never get mounted // (due to e.g. Suspense or error boundaries). // onErrorOrWarning() adds Fibers and recordPendingErrorsAndWarnings() later clears them. const fibersWithChangedErrorOrWarningCounts: Set = new Set(); const pendingFiberToErrorsMap: Map> = new Map(); const pendingFiberToWarningsMap: Map> = new Map(); - // Mapping of fiber IDs to error/warning messages and counts. - const fiberIDToErrorsMap: Map> = new Map(); - const fiberIDToWarningsMap: Map> = new Map(); - function clearErrorsAndWarnings() { // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const id of fiberIDToErrorsMap.keys()) { - const fiber = idToArbitraryFiberMap.get(id); - if (fiber != null) { - fibersWithChangedErrorOrWarningCounts.add(fiber); - updateMostRecentlyInspectedElementIfNecessary(id); - } - } - - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const id of fiberIDToWarningsMap.keys()) { - const fiber = idToArbitraryFiberMap.get(id); - if (fiber != null) { - fibersWithChangedErrorOrWarningCounts.add(fiber); - updateMostRecentlyInspectedElementIfNecessary(id); + for (const devtoolsInstance of idToDevToolsInstanceMap.values()) { + devtoolsInstance.errors = null; + devtoolsInstance.warnings = null; + if (devtoolsInstance.kind === FIBER_INSTANCE) { + fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data); + } else { + // TODO: Handle VirtualInstance. } + updateMostRecentlyInspectedElementIfNecessary(devtoolsInstance.id); } - - fiberIDToErrorsMap.clear(); - fiberIDToWarningsMap.clear(); - flushPendingEvents(); } function clearMessageCountHelper( - fiberID: number, + instanceID: number, pendingFiberToMessageCountMap: Map>, - fiberIDToMessageCountMap: Map>, + forError: boolean, ) { - const fiber = idToArbitraryFiberMap.get(fiberID); - if (fiber != null) { - // Throw out any pending changes. - pendingFiberToErrorsMap.delete(fiber); - - if (fiberIDToMessageCountMap.has(fiberID)) { - fiberIDToMessageCountMap.delete(fiberID); + const devtoolsInstance = idToDevToolsInstanceMap.get(instanceID); + if (devtoolsInstance !== undefined) { + let changed = false; + if (forError) { + if ( + devtoolsInstance.errors !== null && + devtoolsInstance.errors.size > 0 + ) { + changed = true; + } + devtoolsInstance.errors = null; + } else { + if ( + devtoolsInstance.warnings !== null && + devtoolsInstance.warnings.size > 0 + ) { + changed = true; + } + devtoolsInstance.warnings = null; + } + if (devtoolsInstance.kind === FIBER_INSTANCE) { + const fiber = devtoolsInstance.data; + // Throw out any pending changes. + pendingFiberToErrorsMap.delete(fiber); - // If previous flushed counts have changed, schedule an update too. - fibersWithChangedErrorOrWarningCounts.add(fiber); - flushPendingEvents(); + if (changed) { + // If previous flushed counts have changed, schedule an update too. + fibersWithChangedErrorOrWarningCounts.add(fiber); + flushPendingEvents(); - updateMostRecentlyInspectedElementIfNecessary(fiberID); + updateMostRecentlyInspectedElementIfNecessary(instanceID); + } else { + fibersWithChangedErrorOrWarningCounts.delete(fiber); + } } else { - fibersWithChangedErrorOrWarningCounts.delete(fiber); + // TODO: Handle VirtualInstance. } } } - function clearErrorsForElementID(fiberID: number) { - clearMessageCountHelper( - fiberID, - pendingFiberToErrorsMap, - fiberIDToErrorsMap, - ); + function clearErrorsForElementID(instanceID: number) { + clearMessageCountHelper(instanceID, pendingFiberToErrorsMap, true); } - function clearWarningsForElementID(fiberID: number) { - clearMessageCountHelper( - fiberID, - pendingFiberToWarningsMap, - fiberIDToWarningsMap, - ); + function clearWarningsForElementID(instanceID: number) { + clearMessageCountHelper(instanceID, pendingFiberToWarningsMap, false); } function updateMostRecentlyInspectedElementIfNecessary( @@ -845,9 +902,12 @@ export function attach( args: $ReadOnlyArray, ): void { if (type === 'error') { - const maybeID = getFiberIDUnsafe(fiber); + let fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance === undefined && fiber.alternate !== null) { + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate); + } // if this is an error simulated by us to trigger error boundary, ignore - if (maybeID != null && forceErrorForFiberIDs.get(maybeID) === true) { + if (fiberInstance !== undefined && fiberInstance.flags & FORCE_ERROR) { return; } } @@ -1186,39 +1246,24 @@ export function attach( // Returns the unique ID for a Fiber or generates and caches a new one if the Fiber hasn't been seen before. // Once this method has been called for a Fiber, untrackFiberID() should always be called later to avoid leaking. function getOrGenerateFiberID(fiber: Fiber): number { - let id = null; - if (fiberToIDMap.has(fiber)) { - id = fiberToIDMap.get(fiber); - } else { + let fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance === undefined) { const {alternate} = fiber; - if (alternate !== null && fiberToIDMap.has(alternate)) { - id = fiberToIDMap.get(alternate); + if (alternate !== null) { + fiberInstance = fiberToFiberInstanceMap.get(alternate); + if (fiberInstance !== undefined) { + // We found the other pair, so we need to make sure we track the other side. + fiberToFiberInstanceMap.set(fiber, fiberInstance); + } } } let didGenerateID = false; - if (id === null) { + if (fiberInstance === undefined) { didGenerateID = true; - id = getUID(); - } - - // This refinement is for Flow purposes only. - const refinedID = ((id: any): number); - - // Make sure we're tracking this Fiber - // e.g. if it just mounted or an error was logged during initial render. - if (!fiberToIDMap.has(fiber)) { - fiberToIDMap.set(fiber, refinedID); - idToArbitraryFiberMap.set(refinedID, fiber); - } - - // Also make sure we're tracking its alternate, - // e.g. in case this is the first update after mount. - const {alternate} = fiber; - if (alternate !== null) { - if (!fiberToIDMap.has(alternate)) { - fiberToIDMap.set(alternate, refinedID); - } + fiberInstance = createFiberInstance(fiber); + fiberToFiberInstanceMap.set(fiber, fiberInstance); + idToDevToolsInstanceMap.set(fiberInstance.id, fiberInstance); } if (__DEBUG__) { @@ -1232,7 +1277,7 @@ export function attach( } } - return refinedID; + return fiberInstance.id; } // Returns an ID if one has already been generated for the Fiber or throws. @@ -1249,12 +1294,16 @@ export function attach( // Returns an ID if one has already been generated for the Fiber or null if one has not been generated. // Use this method while e.g. logging to avoid over-retaining Fibers. function getFiberIDUnsafe(fiber: Fiber): number | null { - if (fiberToIDMap.has(fiber)) { - return ((fiberToIDMap.get(fiber): any): number); + const fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance !== undefined) { + return fiberInstance.id; } else { const {alternate} = fiber; - if (alternate !== null && fiberToIDMap.has(alternate)) { - return ((fiberToIDMap.get(alternate): any): number); + if (alternate !== null) { + const alternateInstance = fiberToFiberInstanceMap.get(alternate); + if (alternateInstance !== undefined) { + return alternateInstance.id; + } } } return null; @@ -1306,29 +1355,34 @@ export function attach( } untrackFibersSet.forEach(fiber => { - const fiberID = getFiberIDUnsafe(fiber); - if (fiberID !== null) { - idToArbitraryFiberMap.delete(fiberID); + const fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance !== undefined) { + idToDevToolsInstanceMap.delete(fiberInstance.id); // Also clear any errors/warnings associated with this fiber. - clearErrorsForElementID(fiberID); - clearWarningsForElementID(fiberID); + clearErrorsForElementID(fiberInstance.id); + clearWarningsForElementID(fiberInstance.id); + if (fiberInstance.flags & FORCE_ERROR) { + fiberInstance.flags &= ~FORCE_ERROR; + forceErrorCount--; + if (forceErrorCount === 0 && setErrorHandler != null) { + setErrorHandler(shouldErrorFiberAlwaysNull); + } + } + if (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) { + fiberInstance.flags &= ~FORCE_SUSPENSE_FALLBACK; + forceFallbackCount--; + if (forceFallbackCount === 0 && setSuspenseHandler != null) { + setSuspenseHandler(shouldSuspendFiberAlwaysFalse); + } + } } - fiberToIDMap.delete(fiber); - fiberToComponentStackMap.delete(fiber); + fiberToFiberInstanceMap.delete(fiber); const {alternate} = fiber; if (alternate !== null) { - fiberToIDMap.delete(alternate); - fiberToComponentStackMap.delete(alternate); - } - - if (forceErrorForFiberIDs.has(fiberID)) { - forceErrorForFiberIDs.delete(fiberID); - if (forceErrorForFiberIDs.size === 0 && setErrorHandler != null) { - setErrorHandler(shouldErrorFiberAlwaysNull); - } + fiberToFiberInstanceMap.delete(alternate); } }); untrackFibersSet.clear(); @@ -1738,18 +1792,14 @@ export function attach( function reevaluateErrorsAndWarnings() { fibersWithChangedErrorOrWarningCounts.clear(); - fiberIDToErrorsMap.forEach((countMap, fiberID) => { - const fiber = idToArbitraryFiberMap.get(fiberID); - if (fiber != null) { - fibersWithChangedErrorOrWarningCounts.add(fiber); - } - }); - fiberIDToWarningsMap.forEach((countMap, fiberID) => { - const fiber = idToArbitraryFiberMap.get(fiberID); - if (fiber != null) { - fibersWithChangedErrorOrWarningCounts.add(fiber); + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const devtoolsInstance of idToDevToolsInstanceMap.values()) { + if (devtoolsInstance.kind === FIBER_INSTANCE) { + fibersWithChangedErrorOrWarningCounts.add(devtoolsInstance.data); + } else { + // TODO: Handle VirtualInstance. } - }); + } recordPendingErrorsAndWarnings(); } @@ -1757,18 +1807,29 @@ export function attach( fiber: Fiber, fiberID: number, pendingFiberToMessageCountMap: Map>, - fiberIDToMessageCountMap: Map>, + forError: boolean, ): number { let newCount = 0; - let messageCountMap = fiberIDToMessageCountMap.get(fiberID); + const devtoolsInstance = idToDevToolsInstanceMap.get(fiberID); + + if (devtoolsInstance === undefined) { + return 0; + } + + let messageCountMap = forError + ? devtoolsInstance.errors + : devtoolsInstance.warnings; const pendingMessageCountMap = pendingFiberToMessageCountMap.get(fiber); if (pendingMessageCountMap != null) { - if (messageCountMap == null) { + if (messageCountMap === null) { messageCountMap = pendingMessageCountMap; - - fiberIDToMessageCountMap.set(fiberID, pendingMessageCountMap); + if (forError) { + devtoolsInstance.errors = pendingMessageCountMap; + } else { + devtoolsInstance.warnings = pendingMessageCountMap; + } } else { // This Flow refinement should not be necessary and yet... const refinedMessageCountMap = ((messageCountMap: any): Map< @@ -1808,13 +1869,13 @@ export function attach( fiber, fiberID, pendingFiberToErrorsMap, - fiberIDToErrorsMap, + true, ); const warningCount = mergeMapsAndGetCountHelper( fiber, fiberID, pendingFiberToWarningsMap, - fiberIDToWarningsMap, + false, ); pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS); @@ -2830,7 +2891,17 @@ export function attach( function findAllCurrentHostFibers(id: number): $ReadOnlyArray { const fibers = []; - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return fibers; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return fibers; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (!fiber) { return fibers; } @@ -2864,7 +2935,17 @@ export function attach( function findHostInstancesForElementID(id: number) { try { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return null; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return null; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber === null) { return null; } @@ -2878,8 +2959,15 @@ export function attach( } function getDisplayNameForElementID(id: number): null | string { - const fiber = idToArbitraryFiberMap.get(id); - return fiber != null ? getDisplayNameForFiber(fiber) : null; + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + return null; + } + if (devtoolsInstance.kind === FIBER_INSTANCE) { + return getDisplayNameForFiber(devtoolsInstance.data); + } else { + return devtoolsInstance.data.name || ''; + } } function getNearestMountedHostInstance( @@ -2960,13 +3048,10 @@ export function attach( // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberTreeReflection.js // It would be nice if we updated React to inject this function directly (vs just indirectly via findDOMNode). // BEGIN copied code - function findCurrentFiberUsingSlowPathById(id: number): Fiber | null { - const fiber = idToArbitraryFiberMap.get(id); - if (fiber == null) { - console.warn(`Could not find Fiber with id "${id}"`); - return null; - } - + function findCurrentFiberUsingSlowPathByFiberInstance( + fiberInstance: FiberInstance, + ): Fiber | null { + const fiber = fiberInstance.data; const alternate = fiber.alternate; if (!alternate) { // If there is no alternate, then we only need to check if it is mounted. @@ -3126,11 +3211,16 @@ export function attach( } function prepareViewElementSource(id: number): void { - const fiber = idToArbitraryFiberMap.get(id); - if (fiber == null) { - console.warn(`Could not find Fiber with id "${id}"`); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. return; } + const fiber = devtoolsInstance.data; const {elementType, tag, type} = fiber; @@ -3168,7 +3258,17 @@ export function attach( } function getOwnersList(id: number): Array | null { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return null; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return null; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber == null) { return null; } @@ -3197,7 +3297,18 @@ export function attach( let instance = null; let style = null; - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return {instance, style}; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return {instance, style}; + } + + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber !== null) { instance = fiber.stateNode; @@ -3238,7 +3349,17 @@ export function attach( } function inspectElementRaw(id: number): InspectedElement | null { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return null; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return null; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber == null) { return null; } @@ -3427,9 +3548,6 @@ export function attach( rootType = fiberRoot._debugRootType; } - const errors = fiberIDToErrorsMap.get(id) || new Map(); - const warnings = fiberIDToWarningsMap.get(id) || new Map(); - let isErrored = false; let targetErrorBoundaryID; if (isErrorBoundary(fiber)) { @@ -3444,7 +3562,7 @@ export function attach( const DidCapture = 0b000000000000000000010000000; isErrored = (fiber.flags & DidCapture) !== 0 || - forceErrorForFiberIDs.get(id) === true; + (devtoolsInstance.flags & FORCE_ERROR) !== 0; targetErrorBoundaryID = isErrored ? id : getNearestErrorBoundaryID(fiber); } else { targetErrorBoundaryID = getNearestErrorBoundaryID(fiber); @@ -3493,7 +3611,7 @@ export function attach( (!isTimedOutSuspense || // If it's showing fallback because we previously forced it to, // allow toggling it back to remove the fallback override. - forceFallbackForSuspenseIDs.has(id)), + (devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0), // Can view component source location. canViewSource, @@ -3513,8 +3631,14 @@ export function attach( hooks, props: memoizedProps, state: showState ? memoizedState : null, - errors: Array.from(errors.entries()), - warnings: Array.from(warnings.entries()), + errors: + devtoolsInstance.errors === null + ? [] + : Array.from(devtoolsInstance.errors.entries()), + warnings: + devtoolsInstance.warnings === null + ? [] + : Array.from(devtoolsInstance.warnings.entries()), // List of owners owners, @@ -3612,12 +3736,17 @@ export function attach( function updateSelectedElement(inspectedElement: InspectedElement): void { const {hooks, id, props} = inspectedElement; - const fiber = idToArbitraryFiberMap.get(id); - if (fiber == null) { - console.warn(`Could not find Fiber with id "${id}"`); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. return; } + const fiber = devtoolsInstance.data; const {elementType, stateNode, tag, type} = fiber; switch (tag) { @@ -3751,9 +3880,7 @@ export function attach( // Log error & cause for user to debug console.error(message + '\n\n', error); if (error.cause != null) { - const fiber = findCurrentFiberUsingSlowPathById(id); - const componentName = - fiber != null ? getDisplayNameForFiber(fiber) : null; + const componentName = getDisplayNameForElementID(id); console.error( 'React DevTools encountered an error while trying to inspect hooks. ' + 'This is most likely caused by an error in current inspected component' + @@ -3855,7 +3982,7 @@ export function attach( ? mostRecentlyInspectedElement : inspectElementRaw(id); if (result === null) { - console.warn(`Could not find Fiber with id "${id}"`); + console.warn(`Could not find DevToolsInstance with id "${id}"`); return; } @@ -3896,7 +4023,17 @@ export function attach( hookID: ?number, path: Array, ): void { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber !== null) { const instance = fiber.stateNode; @@ -3952,7 +4089,17 @@ export function attach( oldPath: Array, newPath: Array, ): void { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber !== null) { const instance = fiber.stateNode; @@ -4018,7 +4165,17 @@ export function attach( path: Array, value: any, ): void { - const fiber = findCurrentFiberUsingSlowPathById(id); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + console.warn(`Could not find DevToolsInstance with id "${id}"`); + return; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. + return; + } + const fiber = + findCurrentFiberUsingSlowPathByFiberInstance(devtoolsInstance); if (fiber !== null) { const instance = fiber.stateNode; @@ -4281,44 +4438,45 @@ export function attach( return null; } - // Map of id and its force error status: true (error), false (toggled off), - // null (do nothing) - const forceErrorForFiberIDs = new Map(); + let forceErrorCount = 0; - function shouldErrorFiberAccordingToMap(fiber: any) { + function shouldErrorFiberAccordingToMap(fiber: any): null | boolean { if (typeof setErrorHandler !== 'function') { throw new Error( 'Expected overrideError() to not get called for earlier React versions.', ); } - const id = getFiberIDUnsafe(fiber); - if (id === null) { + let fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance === undefined && fiber.alternate !== null) { + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate); + } + if (fiberInstance === undefined) { return null; } - let status = null; - if (forceErrorForFiberIDs.has(id)) { - status = forceErrorForFiberIDs.get(id); - if (status === false) { - // TRICKY overrideError adds entries to this Map, - // so ideally it would be the method that clears them too, - // but that would break the functionality of the feature, - // since DevTools needs to tell React to act differently than it normally would - // (don't just re-render the failed boundary, but reset its errored state too). - // So we can only clear it after telling React to reset the state. - // Technically this is premature and we should schedule it for later, - // since the render could always fail without committing the updated error boundary, - // but since this is a DEV-only feature, the simplicity is worth the trade off. - forceErrorForFiberIDs.delete(id); - - if (forceErrorForFiberIDs.size === 0) { - // Last override is gone. Switch React back to fast path. - setErrorHandler(shouldErrorFiberAlwaysNull); - } + if (fiberInstance.flags & FORCE_ERROR_RESET) { + // TRICKY overrideError adds entries to this Map, + // so ideally it would be the method that clears them too, + // but that would break the functionality of the feature, + // since DevTools needs to tell React to act differently than it normally would + // (don't just re-render the failed boundary, but reset its errored state too). + // So we can only clear it after telling React to reset the state. + // Technically this is premature and we should schedule it for later, + // since the render could always fail without committing the updated error boundary, + // but since this is a DEV-only feature, the simplicity is worth the trade off. + forceErrorCount--; + fiberInstance.flags &= ~FORCE_ERROR_RESET; + if (forceErrorCount === 0) { + // Last override is gone. Switch React back to fast path. + setErrorHandler(shouldErrorFiberAlwaysNull); } + return false; + } else if (fiberInstance.flags & FORCE_ERROR) { + return true; + } else { + return null; } - return status; } function overrideError(id: number, forceError: boolean) { @@ -4331,16 +4489,25 @@ export function attach( ); } - forceErrorForFiberIDs.set(id, forceError); - - if (forceErrorForFiberIDs.size === 1) { - // First override is added. Switch React to slower path. - setErrorHandler(shouldErrorFiberAccordingToMap); + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + return; } + if ((devtoolsInstance.flags & (FORCE_ERROR | FORCE_ERROR_RESET)) === 0) { + forceErrorCount++; + if (forceErrorCount === 1) { + // First override is added. Switch React to slower path. + setErrorHandler(shouldErrorFiberAccordingToMap); + } + } + devtoolsInstance.flags &= forceError ? ~FORCE_ERROR_RESET : ~FORCE_ERROR; + devtoolsInstance.flags |= forceError ? FORCE_ERROR : FORCE_ERROR_RESET; - const fiber = idToArbitraryFiberMap.get(id); - if (fiber != null) { + if (devtoolsInstance.kind === FIBER_INSTANCE) { + const fiber = devtoolsInstance.data; scheduleUpdate(fiber); + } else { + // TODO: Handle VirtualInstance. } } @@ -4348,11 +4515,17 @@ export function attach( return false; } - const forceFallbackForSuspenseIDs = new Set(); + let forceFallbackCount = 0; function shouldSuspendFiberAccordingToSet(fiber: any) { - const maybeID = getFiberIDUnsafe(((fiber: any): Fiber)); - return maybeID !== null && forceFallbackForSuspenseIDs.has(maybeID); + let fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance === undefined && fiber.alternate !== null) { + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate); + } + return ( + fiberInstance !== undefined && + (fiberInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0 + ); } function overrideSuspense(id: number, forceFallback: boolean) { @@ -4364,22 +4537,36 @@ export function attach( 'Expected overrideSuspense() to not get called for earlier React versions.', ); } + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + return; + } + if (forceFallback) { - forceFallbackForSuspenseIDs.add(id); - if (forceFallbackForSuspenseIDs.size === 1) { - // First override is added. Switch React to slower path. - setSuspenseHandler(shouldSuspendFiberAccordingToSet); + if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) === 0) { + devtoolsInstance.flags |= FORCE_SUSPENSE_FALLBACK; + forceFallbackCount++; + if (forceFallbackCount === 1) { + // First override is added. Switch React to slower path. + setSuspenseHandler(shouldSuspendFiberAccordingToSet); + } } } else { - forceFallbackForSuspenseIDs.delete(id); - if (forceFallbackForSuspenseIDs.size === 0) { - // Last override is gone. Switch React back to fast path. - setSuspenseHandler(shouldSuspendFiberAlwaysFalse); + if ((devtoolsInstance.flags & FORCE_SUSPENSE_FALLBACK) !== 0) { + devtoolsInstance.flags &= ~FORCE_SUSPENSE_FALLBACK; + forceFallbackCount--; + if (forceFallbackCount === 0) { + // Last override is gone. Switch React back to fast path. + setSuspenseHandler(shouldSuspendFiberAlwaysFalse); + } } } - const fiber = idToArbitraryFiberMap.get(id); - if (fiber != null) { + + if (devtoolsInstance.kind === FIBER_INSTANCE) { + const fiber = devtoolsInstance.data; scheduleUpdate(fiber); + } else { + // TODO: Handle VirtualInstance. } } @@ -4557,10 +4744,16 @@ export function attach( // The return path will contain Fibers that are "invisible" to the store // because their keys and indexes are important to restoring the selection. function getPathForElement(id: number): Array | null { - let fiber: ?Fiber = idToArbitraryFiberMap.get(id); - if (fiber == null) { + const devtoolsInstance = idToDevToolsInstanceMap.get(id); + if (devtoolsInstance === undefined) { + return null; + } + if (devtoolsInstance.kind !== FIBER_INSTANCE) { + // TODO: Handle VirtualInstance. return null; } + + let fiber: null | Fiber = devtoolsInstance.data; const keyPath = []; while (fiber !== null) { // $FlowFixMe[incompatible-call] found when upgrading Flow @@ -4623,26 +4816,33 @@ export function attach( } function hasElementWithId(id: number): boolean { - return idToArbitraryFiberMap.has(id); + return idToDevToolsInstanceMap.has(id); } function getComponentStackForFiber(fiber: Fiber): string | null { - let componentStack = fiberToComponentStackMap.get(fiber); - if (componentStack == null) { - const dispatcherRef = getDispatcherRef(renderer); - if (dispatcherRef == null) { - return null; - } - - componentStack = getStackByFiberInDevAndProd( - ReactTypeOfWork, - fiber, - dispatcherRef, - ); - fiberToComponentStackMap.set(fiber, componentStack); + // TODO: This should really just take an DevToolsInstance directly. + let fiberInstance = fiberToFiberInstanceMap.get(fiber); + if (fiberInstance === undefined && fiber.alternate !== null) { + fiberInstance = fiberToFiberInstanceMap.get(fiber.alternate); + } + if (fiberInstance === undefined) { + // We're no longer tracking this instance. + return null; + } + if (fiberInstance.componentStack !== null) { + // Cached entry. + return fiberInstance.componentStack; + } + const dispatcherRef = getDispatcherRef(renderer); + if (dispatcherRef == null) { + return null; } - return componentStack; + return (fiberInstance.componentStack = getStackByFiberInDevAndProd( + ReactTypeOfWork, + fiber, + dispatcherRef, + )); } function getSourceForFiber(fiber: Fiber): Source | null {