Renderer
Renderer
import {
ComponentFilterDisplayName,
ComponentFilterElementType,
ComponentFilterHOC,
ComponentFilterLocation,
ElementTypeClass,
ElementTypeContext,
ElementTypeFunction,
ElementTypeForwardRef,
ElementTypeHostComponent,
ElementTypeMemo,
ElementTypeOtherOrUnknown,
ElementTypeProfiler,
ElementTypeRoot,
ElementTypeSuspense,
ElementTypeSuspenseList,
ElementTypeTracingMarker,
StrictMode,
} from 'react-devtools-shared/src/frontend/types';
import {
deletePathInObject,
getDisplayName,
getWrappedDisplayName,
getDefaultComponentFilters,
getInObject,
getUID,
renamePathInObject,
setInObject,
utfEncodeString,
} from 'react-devtools-shared/src/utils';
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
import {
gt,
gte,
serializeToString,
} from 'react-devtools-shared/src/backend/utils';
import {
cleanForBridge,
copyWithDelete,
copyWithRename,
copyWithSet,
getEffectDurations,
} from './utils';
import {
__DEBUG__,
PROFILING_FLAG_BASIC_SUPPORT,
PROFILING_FLAG_TIMELINE_SUPPORT,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
TREE_OPERATION_ADD,
TREE_OPERATION_REMOVE,
TREE_OPERATION_REMOVE_ROOT,
TREE_OPERATION_REORDER_CHILDREN,
TREE_OPERATION_SET_SUBTREE_MODE,
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
} from '../constants';
import {inspectHooksOfFiber} from 'react-debug-tools';
import {
patchConsoleUsingWindowValues,
registerRenderer as registerRendererWithConsole,
patchForStrictMode as patchConsoleForStrictMode,
unpatchForStrictMode as unpatchConsoleForStrictMode,
} from './console';
import {
CONCURRENT_MODE_NUMBER,
CONCURRENT_MODE_SYMBOL_STRING,
DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
PROVIDER_NUMBER,
PROVIDER_SYMBOL_STRING,
CONTEXT_NUMBER,
CONTEXT_SYMBOL_STRING,
STRICT_MODE_NUMBER,
STRICT_MODE_SYMBOL_STRING,
PROFILER_NUMBER,
PROFILER_SYMBOL_STRING,
SCOPE_NUMBER,
SCOPE_SYMBOL_STRING,
FORWARD_REF_NUMBER,
FORWARD_REF_SYMBOL_STRING,
MEMO_NUMBER,
MEMO_SYMBOL_STRING,
SERVER_CONTEXT_SYMBOL_STRING,
} from './ReactSymbols';
import {format} from './utils';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
import {createProfilingHooks} from './profilingHooks';
type ReactPriorityLevelsType = {
ImmediatePriority: number,
UserBlockingPriority: number,
NormalPriority: number,
LowPriority: number,
IdlePriority: number,
NoPriority: number,
};
// Some environments (e.g. React Native / Hermes) don't support the performance API
yet.
const getCurrentTime =
// $FlowFixMe[method-unbinding]
typeof performance === 'object' && typeof performance.now === 'function'
? () => performance.now()
: () => Date.now();
if (gt(version, '17.0.2')) {
ReactPriorityLevels = {
ImmediatePriority: 1,
UserBlockingPriority: 2,
NormalPriority: 3,
LowPriority: 4,
IdlePriority: 5,
NoPriority: 0,
};
}
let StrictModeBits = 0;
if (gte(version, '18.0.0-alpha')) {
// 18+
StrictModeBits = 0b011000;
} else if (gte(version, '16.9.0')) {
// 16.9 - 17
StrictModeBits = 0b1;
} else if (gte(version, '16.3.0')) {
// 16.3 - 16.8
StrictModeBits = 0b10;
}
// **********************************************************
// The section below is copied from files in React repo.
// Keep it in sync, and add version guards if it changes.
//
// TODO Update the gt() check below to be gte() whichever the next version number
is.
// Currently the version in Git is 17.0.2 (but that version has not been/may not
end up being released).
if (gt(version, '17.0.1')) {
ReactTypeOfWork = {
CacheComponent: 24, // Experimental
ClassComponent: 1,
ContextConsumer: 9,
ContextProvider: 10,
CoroutineComponent: -1, // Removed
CoroutineHandlerPhase: -1, // Removed
DehydratedSuspenseComponent: 18, // Behind a flag
ForwardRef: 11,
Fragment: 7,
FunctionComponent: 0,
HostComponent: 5,
HostPortal: 4,
HostRoot: 3,
HostHoistable: 26, // In reality, 18.2+. But doesn't hurt to include it here
HostSingleton: 27, // Same as above
HostText: 6,
IncompleteClassComponent: 17,
IndeterminateComponent: 2,
LazyComponent: 16,
LegacyHiddenComponent: 23,
MemoComponent: 14,
Mode: 8,
OffscreenComponent: 22, // Experimental
Profiler: 12,
ScopeComponent: 21, // Experimental
SimpleMemoComponent: 15,
SuspenseComponent: 13,
SuspenseListComponent: 19, // Experimental
TracingMarkerComponent: 25, // Experimental - This is technically in 18 but
we don't
// want to fork again so we're adding it here instead
YieldComponent: -1, // Removed
};
} else if (gte(version, '17.0.0-alpha')) {
ReactTypeOfWork = {
CacheComponent: -1, // Doesn't exist yet
ClassComponent: 1,
ContextConsumer: 9,
ContextProvider: 10,
CoroutineComponent: -1, // Removed
CoroutineHandlerPhase: -1, // Removed
DehydratedSuspenseComponent: 18, // Behind a flag
ForwardRef: 11,
Fragment: 7,
FunctionComponent: 0,
HostComponent: 5,
HostPortal: 4,
HostRoot: 3,
HostHoistable: -1, // Doesn't exist yet
HostSingleton: -1, // Doesn't exist yet
HostText: 6,
IncompleteClassComponent: 17,
IndeterminateComponent: 2,
LazyComponent: 16,
LegacyHiddenComponent: 24,
MemoComponent: 14,
Mode: 8,
OffscreenComponent: 23, // Experimental
Profiler: 12,
ScopeComponent: 21, // Experimental
SimpleMemoComponent: 15,
SuspenseComponent: 13,
SuspenseListComponent: 19, // Experimental
TracingMarkerComponent: -1, // Doesn't exist yet
YieldComponent: -1, // Removed
};
} else if (gte(version, '16.6.0-beta.0')) {
ReactTypeOfWork = {
CacheComponent: -1, // Doesn't exist yet
ClassComponent: 1,
ContextConsumer: 9,
ContextProvider: 10,
CoroutineComponent: -1, // Removed
CoroutineHandlerPhase: -1, // Removed
DehydratedSuspenseComponent: 18, // Behind a flag
ForwardRef: 11,
Fragment: 7,
FunctionComponent: 0,
HostComponent: 5,
HostPortal: 4,
HostRoot: 3,
HostHoistable: -1, // Doesn't exist yet
HostSingleton: -1, // Doesn't exist yet
HostText: 6,
IncompleteClassComponent: 17,
IndeterminateComponent: 2,
LazyComponent: 16,
LegacyHiddenComponent: -1,
MemoComponent: 14,
Mode: 8,
OffscreenComponent: -1, // Experimental
Profiler: 12,
ScopeComponent: -1, // Experimental
SimpleMemoComponent: 15,
SuspenseComponent: 13,
SuspenseListComponent: 19, // Experimental
TracingMarkerComponent: -1, // Doesn't exist yet
YieldComponent: -1, // Removed
};
} else if (gte(version, '16.4.3-alpha')) {
ReactTypeOfWork = {
CacheComponent: -1, // Doesn't exist yet
ClassComponent: 2,
ContextConsumer: 11,
ContextProvider: 12,
CoroutineComponent: -1, // Removed
CoroutineHandlerPhase: -1, // Removed
DehydratedSuspenseComponent: -1, // Doesn't exist yet
ForwardRef: 13,
Fragment: 9,
FunctionComponent: 0,
HostComponent: 7,
HostPortal: 6,
HostRoot: 5,
HostHoistable: -1, // Doesn't exist yet
HostSingleton: -1, // Doesn't exist yet
HostText: 8,
IncompleteClassComponent: -1, // Doesn't exist yet
IndeterminateComponent: 4,
LazyComponent: -1, // Doesn't exist yet
LegacyHiddenComponent: -1,
MemoComponent: -1, // Doesn't exist yet
Mode: 10,
OffscreenComponent: -1, // Experimental
Profiler: 15,
ScopeComponent: -1, // Experimental
SimpleMemoComponent: -1, // Doesn't exist yet
SuspenseComponent: 16,
SuspenseListComponent: -1, // Doesn't exist yet
TracingMarkerComponent: -1, // Doesn't exist yet
YieldComponent: -1, // Removed
};
} else {
ReactTypeOfWork = {
CacheComponent: -1, // Doesn't exist yet
ClassComponent: 2,
ContextConsumer: 12,
ContextProvider: 13,
CoroutineComponent: 7,
CoroutineHandlerPhase: 8,
DehydratedSuspenseComponent: -1, // Doesn't exist yet
ForwardRef: 14,
Fragment: 10,
FunctionComponent: 1,
HostComponent: 5,
HostPortal: 4,
HostRoot: 3,
HostHoistable: -1, // Doesn't exist yet
HostSingleton: -1, // Doesn't exist yet
HostText: 6,
IncompleteClassComponent: -1, // Doesn't exist yet
IndeterminateComponent: 0,
LazyComponent: -1, // Doesn't exist yet
LegacyHiddenComponent: -1,
MemoComponent: -1, // Doesn't exist yet
Mode: 11,
OffscreenComponent: -1, // Experimental
Profiler: 15,
ScopeComponent: -1, // Experimental
SimpleMemoComponent: -1, // Doesn't exist yet
SuspenseComponent: 16,
SuspenseListComponent: -1, // Doesn't exist yet
TracingMarkerComponent: -1, // Doesn't exist yet
YieldComponent: 9,
};
}
// **********************************************************
// End of copied code.
// **********************************************************
const {
CacheComponent,
ClassComponent,
IncompleteClassComponent,
FunctionComponent,
IndeterminateComponent,
ForwardRef,
HostRoot,
HostHoistable,
HostSingleton,
HostComponent,
HostPortal,
HostText,
Fragment,
LazyComponent,
LegacyHiddenComponent,
MemoComponent,
OffscreenComponent,
Profiler,
ScopeComponent,
SimpleMemoComponent,
SuspenseComponent,
SuspenseListComponent,
TracingMarkerComponent,
} = ReactTypeOfWork;
switch (tag) {
case CacheComponent:
return 'Cache';
case ClassComponent:
case IncompleteClassComponent:
return getDisplayName(resolvedType);
case FunctionComponent:
case IndeterminateComponent:
return getDisplayName(resolvedType);
case ForwardRef:
return getWrappedDisplayName(
elementType,
resolvedType,
'ForwardRef',
'Anonymous',
);
case HostRoot:
const fiberRoot = fiber.stateNode;
if (fiberRoot != null && fiberRoot._debugRootType !== null) {
return fiberRoot._debugRootType;
}
return null;
case HostComponent:
case HostSingleton:
case HostHoistable:
return type;
case HostPortal:
case HostText:
return null;
case Fragment:
return 'Fragment';
case LazyComponent:
// This display name will not be user visible.
// Once a Lazy component loads its inner component, React replaces the tag
and type.
// This display name will only show up in console logs when DevTools DEBUG
mode is on.
return 'Lazy';
case MemoComponent:
case SimpleMemoComponent:
// Display name in React does not use `Memo` as a wrapper but fallback
name.
return getWrappedDisplayName(
elementType,
resolvedType,
'Memo',
'Anonymous',
);
case SuspenseComponent:
return 'Suspense';
case LegacyHiddenComponent:
return 'LegacyHidden';
case OffscreenComponent:
return 'Offscreen';
case ScopeComponent:
return 'Scope';
case SuspenseListComponent:
return 'SuspenseList';
case Profiler:
return 'Profiler';
case TracingMarkerComponent:
return 'TracingMarker';
default:
const typeSymbol = getTypeSymbol(type);
switch (typeSymbol) {
case CONCURRENT_MODE_NUMBER:
case CONCURRENT_MODE_SYMBOL_STRING:
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
return null;
case PROVIDER_NUMBER:
case PROVIDER_SYMBOL_STRING:
// 16.3.0 exposed the context object as "context"
// PR #12501 changed it to "_context" for 16.3.1+
// NOTE Keep in sync with inspectElementRaw()
resolvedContext = fiber.type._context || fiber.type.context;
return `${resolvedContext.displayName || 'Context'}.Provider`;
case CONTEXT_NUMBER:
case CONTEXT_SYMBOL_STRING:
case SERVER_CONTEXT_SYMBOL_STRING:
// 16.3-16.5 read from "type" because the Consumer is the actual
context object.
// 16.6+ should read from "type._context" because Consumer can be
different (in DEV).
// NOTE Keep in sync with inspectElementRaw()
resolvedContext = fiber.type._context || fiber.type;
return {
getDisplayNameForFiber,
getTypeSymbol,
ReactPriorityLevels,
ReactTypeOfWork,
StrictModeBits,
};
}
const {
getDisplayNameForFiber,
getTypeSymbol,
ReactPriorityLevels,
ReactTypeOfWork,
StrictModeBits,
} = getInternalReactConstants(version);
const {
CacheComponent,
ClassComponent,
ContextConsumer,
DehydratedSuspenseComponent,
ForwardRef,
Fragment,
FunctionComponent,
HostRoot,
HostHoistable,
HostSingleton,
HostPortal,
HostComponent,
HostText,
IncompleteClassComponent,
IndeterminateComponent,
LegacyHiddenComponent,
MemoComponent,
OffscreenComponent,
SimpleMemoComponent,
SuspenseComponent,
SuspenseListComponent,
TracingMarkerComponent,
} = ReactTypeOfWork;
const {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
LowPriority,
IdlePriority,
NoPriority,
} = ReactPriorityLevels;
const {
getLaneLabelMap,
injectProfilingHooks,
overrideHookState,
overrideHookStateDeletePath,
overrideHookStateRenamePath,
overrideProps,
overridePropsDeletePath,
overridePropsRenamePath,
scheduleRefresh,
setErrorHandler,
setSuspenseHandler,
scheduleUpdate,
} = renderer;
const supportsTogglingError =
typeof setErrorHandler === 'function' &&
typeof scheduleUpdate === 'function';
const supportsTogglingSuspense =
typeof setSuspenseHandler === 'function' &&
typeof scheduleUpdate === 'function';
// Pass the Profiling hooks to the reconciler for it to call during render.
injectProfilingHooks(response.profilingHooks);
// Hang onto this toggle so we can notify the external methods of profiling
status changes.
getTimelineData = response.getTimelineData;
toggleProfilingStatus = response.toggleProfilingStatus;
}
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);
}
}
fiberIDToErrorsMap.clear();
fiberIDToWarningsMap.clear();
flushPendingEvents();
}
function clearMessageCountHelper(
fiberID: number,
pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
fiberIDToMessageCountMap: Map<number, Map<string, number>>,
) {
const fiber = idToArbitraryFiberMap.get(fiberID);
if (fiber != null) {
// Throw out any pending changes.
pendingFiberToErrorsMap.delete(fiber);
if (fiberIDToMessageCountMap.has(fiberID)) {
fiberIDToMessageCountMap.delete(fiberID);
updateMostRecentlyInspectedElementIfNecessary(fiberID);
} else {
fibersWithChangedErrorOrWarningCounts.delete(fiber);
}
}
}
// Mark this Fiber as needed its warning/error count updated during the next
flush.
fibersWithChangedErrorOrWarningCounts.add(fiber);
const debug = (
name: string,
fiber: Fiber,
parentFiber: ?Fiber,
extraString: string = '',
): void => {
if (__DEBUG__) {
const displayName =
fiber.tag + ':' + (getDisplayNameForFiber(fiber) || 'null');
console.groupCollapsed(
`[renderer] %c${name} %c${displayName} (${maybeID}) %c${
parentFiber ? `${parentDisplayName} (${maybeParentID})` : ''
} %c${extraString}`,
'color: red; font-weight: bold;',
'color: blue;',
'color: purple;',
'color: black;',
);
console.log(new Error().stack.split('\n').slice(1).join('\n'));
console.groupEnd();
}
};
// Highlight updates
let traceUpdatesEnabled: boolean = false;
const traceUpdatesForNodes: Set<NativeType> = new Set();
componentFilters.forEach(componentFilter => {
if (!componentFilter.isEnabled) {
return;
}
switch (componentFilter.type) {
case ComponentFilterDisplayName:
if (componentFilter.isValid && componentFilter.value !== '') {
hideElementsWithDisplayNames.add(
new RegExp(componentFilter.value, 'i'),
);
}
break;
case ComponentFilterElementType:
hideElementsWithTypes.add(componentFilter.value);
break;
case ComponentFilterLocation:
if (componentFilter.isValid && componentFilter.value !== '') {
hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));
}
break;
case ComponentFilterHOC:
hideElementsWithDisplayNames.add(new RegExp('\\('));
break;
default:
console.warn(
`Invalid component filter type "${componentFilter.type}"`,
);
break;
}
});
}
applyComponentFilters(componentFilters);
// Also re-evaluate all error and warning counts given the new filters.
reevaluateErrorsAndWarnings();
flushPendingEvents();
}
switch (tag) {
case DehydratedSuspenseComponent:
// TODO: ideally we would show dehydrated Suspense immediately.
// However, it has some special behavior (like disconnecting
// an alternate and turning into real Suspense) which breaks DevTools.
// For now, ignore it, and only show it once it gets hydrated.
// https://github.com/bvaughn/react-devtools-experimental/issues/197
return true;
case HostPortal:
case HostText:
case LegacyHiddenComponent:
case OffscreenComponent:
return true;
case HostRoot:
// It is never valid to filter the root element.
return false;
case Fragment:
return key === null;
default:
const typeSymbol = getTypeSymbol(type);
switch (typeSymbol) {
case CONCURRENT_MODE_NUMBER:
case CONCURRENT_MODE_SYMBOL_STRING:
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
case STRICT_MODE_NUMBER:
case STRICT_MODE_SYMBOL_STRING:
return true;
default:
break;
}
}
if (hideElementsWithDisplayNames.size > 0) {
const displayName = getDisplayNameForFiber(fiber);
if (displayName != null) {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const displayNameRegExp of hideElementsWithDisplayNames) {
if (displayNameRegExp.test(displayName)) {
return true;
}
}
}
}
return false;
}
switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
return ElementTypeClass;
case FunctionComponent:
case IndeterminateComponent:
return ElementTypeFunction;
case ForwardRef:
return ElementTypeForwardRef;
case HostRoot:
return ElementTypeRoot;
case HostComponent:
case HostHoistable:
case HostSingleton:
return ElementTypeHostComponent;
case HostPortal:
case HostText:
case Fragment:
return ElementTypeOtherOrUnknown;
case MemoComponent:
case SimpleMemoComponent:
return ElementTypeMemo;
case SuspenseComponent:
return ElementTypeSuspense;
case SuspenseListComponent:
return ElementTypeSuspenseList;
case TracingMarkerComponent:
return ElementTypeTracingMarker;
default:
const typeSymbol = getTypeSymbol(type);
switch (typeSymbol) {
case CONCURRENT_MODE_NUMBER:
case CONCURRENT_MODE_SYMBOL_STRING:
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
return ElementTypeOtherOrUnknown;
case PROVIDER_NUMBER:
case PROVIDER_SYMBOL_STRING:
return ElementTypeContext;
case CONTEXT_NUMBER:
case CONTEXT_SYMBOL_STRING:
return ElementTypeContext;
case STRICT_MODE_NUMBER:
case STRICT_MODE_SYMBOL_STRING:
return ElementTypeOtherOrUnknown;
case PROFILER_NUMBER:
case PROFILER_SYMBOL_STRING:
return ElementTypeProfiler;
default:
return ElementTypeOtherOrUnknown;
}
}
}
// When profiling is supported, we store the latest tree base durations for each
Fiber.
// This is so that we can quickly capture a snapshot of those values if profiling
starts.
// If we didn't store these values, we'd have to crawl the tree when profiling
started,
// and use a slow path to find each of the current Fibers.
const idToTreeBaseDurationMap: Map<number, number> = new Map();
// When profiling is supported, we store the latest tree base durations for each
Fiber.
// This map enables us to filter these times by root when sending them to the
frontend.
const idToRootMap: Map<number, number> = new Map();
// When a mount or update is in progress, this value tracks the root that is
being operated on.
let currentRootID: number = -1;
// 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 {
const {alternate} = fiber;
if (alternate !== null && fiberToIDMap.has(alternate)) {
id = fiberToIDMap.get(alternate);
}
}
if (__DEBUG__) {
if (didGenerateID) {
debug(
'getOrGenerateFiberID()',
fiber,
fiber.return,
'Generated a new UID',
);
}
}
return refinedID;
}
// Returns an ID if one has already been generated for the Fiber or throws.
function getFiberIDThrows(fiber: Fiber): number {
const maybeID = getFiberIDUnsafe(fiber);
if (maybeID !== null) {
return maybeID;
}
throw Error(
`Could not find ID for Fiber "${getDisplayNameForFiber(fiber) || ''}"`,
);
}
// 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);
} else {
const {alternate} = fiber;
if (alternate !== null && fiberToIDMap.has(alternate)) {
return ((fiberToIDMap.get(alternate): any): number);
}
}
return null;
}
// Removes a Fiber (and its alternate) from the Maps used to track their id.
// This method should always be called when a Fiber is unmounting.
function untrackFiberID(fiber: Fiber) {
if (__DEBUG__) {
debug('untrackFiberID()', fiber, fiber.return, 'schedule after delay');
}
// Untrack Fibers after a slight delay in order to support a Fast Refresh edge
case:
// 1. Component type is updated and Fast Refresh schedules an update+remount.
// 2. flushPendingErrorsAndWarningsAfterDelay() runs, sees the old Fiber is no
longer mounted
// (it's been disconnected by Fast Refresh), and calls untrackFiberID() to
clear it from the Map.
// 3. React flushes pending passive effects before it runs the next render,
// which logs an error or warning, which causes a new ID to be generated for
this Fiber.
// 4. DevTools now tries to unmount the old Component with the new ID.
//
// The underlying problem here is the premature clearing of the Fiber ID,
// but DevTools has no way to detect that a given Fiber has been scheduled for
Fast Refresh.
// (The "_debugNeedsRemount" flag won't necessarily be set.)
//
// The best we can do is to delay untracking by a small amount,
// and give React time to process the Fast Refresh delay.
untrackFibersSet.add(fiber);
function untrackFibers() {
if (untrackFibersTimeoutID !== null) {
clearTimeout(untrackFibersTimeoutID);
untrackFibersTimeoutID = null;
}
untrackFibersSet.forEach(fiber => {
const fiberID = getFiberIDUnsafe(fiber);
if (fiberID !== null) {
idToArbitraryFiberMap.delete(fiberID);
fiberToIDMap.delete(fiber);
if (forceErrorForFiberIDs.has(fiberID)) {
forceErrorForFiberIDs.delete(fiberID);
if (forceErrorForFiberIDs.size === 0 && setErrorHandler != null) {
setErrorHandler(shouldErrorFiberAlwaysNull);
}
}
});
untrackFibersSet.clear();
}
function getChangeDescription(
prevFiber: Fiber | null,
nextFiber: Fiber,
): ChangeDescription | null {
switch (getElementTypeForFiber(nextFiber)) {
case ElementTypeClass:
case ElementTypeFunction:
case ElementTypeMemo:
case ElementTypeForwardRef:
if (prevFiber === null) {
return {
context: null,
didHooksChange: false,
isFirstMount: true,
props: null,
state: null,
};
} else {
const data: ChangeDescription = {
context: getContextChangedKeys(nextFiber),
didHooksChange: false,
isFirstMount: false,
props: getChangedKeys(
prevFiber.memoizedProps,
nextFiber.memoizedProps,
),
state: getChangedKeys(
prevFiber.memoizedState,
nextFiber.memoizedState,
),
};
// Only traverse the hooks list once, depending on what info we're
returning.
const indices = getChangedHooksIndices(
prevFiber.memoizedState,
nextFiber.memoizedState,
);
data.hooks = indices;
data.didHooksChange = indices !== null && indices.length > 0;
return data;
}
default:
return null;
}
}
switch (getElementTypeForFiber(fiber)) {
case ElementTypeClass:
const instance = fiber.stateNode;
if (instance != null) {
if (
instance.constructor &&
instance.constructor.contextType != null
) {
modernContext = instance.context;
} else {
legacyContext = instance.context;
if (legacyContext && Object.keys(legacyContext).length === 0) {
legacyContext = NO_CONTEXT;
}
}
}
return [legacyContext, modernContext];
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
const dependencies = fiber.dependencies;
if (dependencies && dependencies.firstContext) {
modernContext = dependencies.firstContext;
}
switch (getElementTypeForFiber(fiber)) {
case ElementTypeClass:
if (prevContexts && nextContexts) {
if (nextLegacyContext !== NO_CONTEXT) {
return getChangedKeys(prevLegacyContext, nextLegacyContext);
} else if (nextModernContext !== NO_CONTEXT) {
return prevModernContext !== nextModernContext;
}
}
break;
case ElementTypeForwardRef:
case ElementTypeFunction:
case ElementTypeMemo:
if (nextModernContext !== NO_CONTEXT) {
let prevContext = prevModernContext;
let nextContext = nextModernContext;
prevContext = prevContext.next;
nextContext = nextContext.next;
}
return false;
}
break;
default:
break;
}
}
return null;
}
// Detect useSyncExternalStore()
return (
boundHasOwnProperty('value') &&
boundHasOwnProperty('getSnapshot') &&
typeof queue.getSnapshot === 'function'
);
}
if (isHookThatCanScheduleUpdate(prev)) {
return prevMemoizedState !== nextMemoizedState;
}
return false;
}
return indices;
}
return changedKeys;
}
// eslint-disable-next-line no-unused-vars
function didFiberRender(prevFiber: Fiber, nextFiber: Fiber): boolean {
switch (nextFiber.tag) {
case ClassComponent:
case FunctionComponent:
case ContextConsumer:
case MemoComponent:
case SimpleMemoComponent:
case ForwardRef:
// For types that execute user code, we check PerformedWork effect.
// We don't reflect bailouts (either referential or sCU) in DevTools.
// TODO: This flag is a leaked implementation detail. Once we start
// releasing DevTools in lockstep with React, we should import a
// function from the reconciler instead.
const PerformedWork = 0b000000000000000000000000001;
return (getFiberFlags(nextFiber) & PerformedWork) === PerformedWork;
// Note: ContextConsumer only gets PerformedWork effect in 16.3.3+
// so it won't get highlighted with React 16.3.0 to 16.3.2.
default:
// For host components and other types, we compare inputs
// to determine whether something is an update.
return (
prevFiber.memoizedProps !== nextFiber.memoizedProps ||
prevFiber.memoizedState !== nextFiber.memoizedState ||
prevFiber.ref !== nextFiber.ref
);
}
}
type StringTableEntry = {
encodedString: Array<number>,
id: number,
};
function shouldBailoutWithPendingOperations() {
if (isProfiling) {
if (
currentCommitProfilingMetadata != null &&
currentCommitProfilingMetadata.durations.length > 0
) {
return false;
}
}
return (
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingSimulatedUnmountedIDs.length === 0 &&
pendingUnmountedRootID === null
);
}
function clearPendingErrorsAndWarningsAfterDelay() {
if (flushPendingErrorsAndWarningsAfterDelayTimeoutID !== null) {
clearTimeout(flushPendingErrorsAndWarningsAfterDelayTimeoutID);
flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
}
}
function flushPendingErrorsAndWarningsAfterDelay() {
clearPendingErrorsAndWarningsAfterDelay();
flushPendingErrorsAndWarningsAfterDelayTimeoutID = setTimeout(() => {
flushPendingErrorsAndWarningsAfterDelayTimeoutID = null;
if (pendingOperations.length > 0) {
// On the off chance that something else has pushed pending operations,
// we should bail on warnings; it's probably not safe to push midway.
return;
}
recordPendingErrorsAndWarnings();
if (shouldBailoutWithPendingOperations()) {
// No warnings or errors to flush; we can bail out early here too.
return;
}
flushOrQueueOperations(operations);
pendingOperations.length = 0;
}, 1000);
}
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);
}
});
recordPendingErrorsAndWarnings();
}
function mergeMapsAndGetCountHelper(
fiber: Fiber,
fiberID: number,
pendingFiberToMessageCountMap: Map<Fiber, Map<string, number>>,
fiberIDToMessageCountMap: Map<number, Map<string, number>>,
): number {
let newCount = 0;
fiberIDToMessageCountMap.set(fiberID, pendingMessageCountMap);
} else {
// This Flow refinement should not be necessary and yet...
const refinedMessageCountMap = ((messageCountMap: any): Map<
string,
number,
>);
if (!shouldFilterFiber(fiber)) {
if (messageCountMap != null) {
messageCountMap.forEach(count => {
newCount += count;
});
}
}
pendingFiberToMessageCountMap.delete(fiber);
return newCount;
}
function recordPendingErrorsAndWarnings() {
clearPendingErrorsAndWarningsAfterDelay();
fibersWithChangedErrorOrWarningCounts.forEach(fiber => {
const fiberID = getFiberIDUnsafe(fiber);
if (fiberID === null) {
// Don't send updates for Fibers that didn't mount due to e.g. Suspense or
an error boundary.
} else {
const errorCount = mergeMapsAndGetCountHelper(
fiber,
fiberID,
pendingFiberToErrorsMap,
fiberIDToErrorsMap,
);
const warningCount = mergeMapsAndGetCountHelper(
fiber,
fiberID,
pendingFiberToWarningsMap,
fiberIDToWarningsMap,
);
pushOperation(TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS);
pushOperation(fiberID);
pushOperation(errorCount);
pushOperation(warningCount);
}
if (shouldBailoutWithPendingOperations()) {
// If we aren't profiling, we can just bail out here.
// No use sending an empty update over the bridge.
//
// The Profiler stores metadata for each commit and reconstructs the app tree
per commit using:
// (1) an initial tree snapshot and
// (2) the operations array for each commit
// Because of this, it's important that the operations and metadata arrays
align,
// So it's important not to omit even empty operations while profiling is
active.
return;
}
const numUnmountIDs =
pendingRealUnmountedIDs.length +
pendingSimulatedUnmountedIDs.length +
(pendingUnmountedRootID === null ? 0 : 1);
operations[i++] = length;
for (let j = 0; j < length; j++) {
operations[i + j] = encodedString[j];
}
i += length;
});
if (numUnmountIDs > 0) {
// All unmounts except roots are batched in a single message.
operations[i++] = TREE_OPERATION_REMOVE;
// The first number is how many unmounted IDs we're gonna send.
operations[i++] = numUnmountIDs;
// Fill in the real unmounts in the reverse order.
// They were inserted parents-first by React, but we want children-first.
// So we traverse our array backwards.
for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
operations[i++] = pendingRealUnmountedIDs[j];
}
// Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
// (We want children to go before parents.)
// They go *after* the real unmounts because we know for sure they won't be
// children of already pushed "real" IDs. If they were, we wouldn't be able
// to discover them during the traversal, as they would have been deleted.
for (let j = 0; j < pendingSimulatedUnmountedIDs.length; j++) {
operations[i + j] = pendingSimulatedUnmountedIDs[j];
}
i += pendingSimulatedUnmountedIDs.length;
// The root ID should always be unmounted last.
if (pendingUnmountedRootID !== null) {
operations[i] = pendingUnmountedRootID;
i++;
}
}
// Fill in the rest of the operations.
for (let j = 0; j < pendingOperations.length; j++) {
operations[i + j] = pendingOperations[j];
}
i += pendingOperations.length;
// Reset all of the pending state now that we've told the frontend about it.
pendingOperations.length = 0;
pendingRealUnmountedIDs.length = 0;
pendingSimulatedUnmountedIDs.length = 0;
pendingUnmountedRootID = null;
pendingStringTable.clear();
pendingStringTableLength = 0;
}
const id = pendingStringTable.size + 1;
const encodedString = utfEncodeString(string);
pendingStringTable.set(string, {
encodedString,
id,
});
// The string table total length needs to account both for the string length,
// and for the array item that contains the length itself.
//
// Don't use string length for this table.
// It won't work for multibyte characters (like emoji).
pendingStringTableLength += encodedString.length + 1;
return id;
}
if (__DEBUG__) {
debug('recordMount()', fiber, parentFiber);
}
// Adding a new field here would require a bridge protocol version bump (a
backwads breaking change).
// Instead let's re-purpose a pre-existing field to carry more information.
let profilingFlags = 0;
if (isProfilingSupported) {
profilingFlags = PROFILING_FLAG_BASIC_SUPPORT;
if (typeof injectProfilingHooks === 'function') {
profilingFlags |= PROFILING_FLAG_TIMELINE_SUPPORT;
}
}
if (isRoot) {
pushOperation(TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(ElementTypeRoot);
pushOperation((fiber.mode & StrictModeBits) !== 0 ? 1 : 0);
pushOperation(profilingFlags);
pushOperation(StrictModeBits !== 0 ? 1 : 0);
pushOperation(hasOwnerMetadata ? 1 : 0);
if (isProfiling) {
if (displayNamesByRootID !== null) {
displayNamesByRootID.set(id, getDisplayNameForRoot(fiber));
}
}
} else {
const {key} = fiber;
const displayName = getDisplayNameForFiber(fiber);
const elementType = getElementTypeForFiber(fiber);
const {_debugOwner} = fiber;
// This check is a guard to handle a React element that has been modified
// in such a way as to bypass the default stringification of the "key"
property.
const keyString = key === null ? null : String(key);
const keyStringID = getStringID(keyString);
pushOperation(TREE_OPERATION_ADD);
pushOperation(id);
pushOperation(elementType);
pushOperation(parentID);
pushOperation(ownerID);
pushOperation(displayNameStringID);
pushOperation(keyStringID);
if (isProfilingSupported) {
idToRootMap.set(id, currentRootID);
recordProfilingDurations(fiber);
}
}
function recordUnmount(fiber: Fiber, isSimulated: boolean) {
if (__DEBUG__) {
debug(
'recordUnmount()',
fiber,
null,
isSimulated ? 'unmount is simulated' : '',
);
}
// Flow refinement.
const id = ((unsafeID: any): number);
const isRoot = fiber.tag === HostRoot;
if (isRoot) {
// Roots must be removed only after all children (pending and simulated) have
been removed.
// So we track it separately.
pendingUnmountedRootID = id;
} else if (!shouldFilterFiber(fiber)) {
// To maintain child-first ordering,
// we'll push it into one of these queues,
// and later arrange them in the correct order.
if (isSimulated) {
pendingSimulatedUnmountedIDs.push(id);
} else {
pendingRealUnmountedIDs.push(id);
}
}
if (!fiber._debugNeedsRemount) {
untrackFiberID(fiber);
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
idToRootMap.delete(id);
idToTreeBaseDurationMap.delete(id);
}
}
}
function mountFiberRecursively(
firstChild: Fiber,
parentFiber: Fiber | null,
traverseSiblings: boolean,
traceNearestHostComponentUpdate: boolean,
) {
// Iterate over siblings rather than recursing.
// This reduces the chance of stack overflow for wide trees (e.g. lists with
many items).
let fiber: Fiber | null = firstChild;
while (fiber !== null) {
// Generate an ID even for filtered Fibers, in case it's needed later (e.g.
for Profiling).
getOrGenerateFiberID(fiber);
if (__DEBUG__) {
debug('mountFiberRecursively()', fiber, parentFiber);
}
// If we have the tree selection from previous reload, try to match this
Fiber.
// Also remember whether to do the same for siblings.
const mightSiblingsBeOnTrackedPath =
updateTrackedPathStateBeforeMount(fiber);
if (traceUpdatesEnabled) {
if (traceNearestHostComponentUpdate) {
const elementType = getElementTypeForFiber(fiber);
// If an ancestor updated, we should mark the nearest host nodes for
highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(fiber.stateNode);
traceNearestHostComponentUpdate = false;
}
}
if (isProfiling) {
const {alternate} = fiber;
if (recordChangeDescriptions) {
const changeDescription = getChangeDescription(alternate, fiber);
if (changeDescription !== null) {
if (metadata.changeDescriptions !== null) {
metadata.changeDescriptions.set(id, changeDescription);
}
}
updateContextsForFiber(fiber);
}
}
}
}
}
function findReorderedChildrenRecursively(
fiber: Fiber,
nextChildren: Array<number>,
) {
if (!shouldFilterFiber(fiber)) {
nextChildren.push(getFiberIDThrows(fiber));
} else {
let child = fiber.child;
const isTimedOutSuspense =
fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
if (isTimedOutSuspense) {
// Special case: if Suspense mounts in a timed-out state,
// get the fallback child from the inner fragment,
// and skip over the primary child.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment
? primaryChildFragment.sibling
: null;
const fallbackChild = fallbackChildFragment
? fallbackChildFragment.child
: null;
if (fallbackChild !== null) {
child = fallbackChild;
}
}
while (child !== null) {
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
}
}
}
// Returns whether closest unfiltered fiber parent needs to reset its child list.
function updateFiberRecursively(
nextFiber: Fiber,
prevFiber: Fiber,
parentFiber: Fiber | null,
traceNearestHostComponentUpdate: boolean,
): boolean {
const id = getOrGenerateFiberID(nextFiber);
if (__DEBUG__) {
debug('updateFiberRecursively()', nextFiber, parentFiber);
}
if (traceUpdatesEnabled) {
const elementType = getElementTypeForFiber(nextFiber);
if (traceNearestHostComponentUpdate) {
// If an ancestor updated, we should mark the nearest host nodes for
highlighting.
if (elementType === ElementTypeHostComponent) {
traceUpdatesForNodes.add(nextFiber.stateNode);
traceNearestHostComponentUpdate = false;
}
} else {
if (
elementType === ElementTypeFunction ||
elementType === ElementTypeClass ||
elementType === ElementTypeContext ||
elementType === ElementTypeMemo ||
elementType === ElementTypeForwardRef
) {
// Otherwise if this is a traced ancestor, flag for the nearest host
descendant(s).
traceNearestHostComponentUpdate = didFiberRender(
prevFiber,
nextFiber,
);
}
}
}
if (
mostRecentlyInspectedElement !== null &&
mostRecentlyInspectedElement.id === id &&
didFiberRender(prevFiber, nextFiber)
) {
// If this Fiber has updated, clear cached inspected data.
// If it is inspected again, it may need to be re-run to obtain updated hooks
values.
hasElementUpdatedSinceLastInspected = true;
}
shouldResetChildren = true;
}
if (
nextFallbackChildSet != null &&
prevFallbackChildSet != null &&
updateFiberRecursively(
nextFallbackChildSet,
prevFallbackChildSet,
nextFiber,
traceNearestHostComponentUpdate,
)
) {
shouldResetChildren = true;
}
} else if (prevDidTimeout && !nextDidTimeOut) {
// Fallback -> Primary:
// 1. Unmount fallback set
// Note: don't emulate fallback unmount because React actually did it.
// 2. Mount primary set
const nextPrimaryChildSet = nextFiber.child;
if (nextPrimaryChildSet !== null) {
mountFiberRecursively(
nextPrimaryChildSet,
shouldIncludeInTree ? nextFiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
}
shouldResetChildren = true;
} else if (!prevDidTimeout && nextDidTimeOut) {
// Primary -> Fallback:
// 1. Hide primary set
// This is not a real unmount, so it won't get reported by React.
// We need to manually walk the previous tree and record unmounts.
unmountFiberChildrenRecursively(prevFiber);
// 2. Mount fallback set
const nextFiberChild = nextFiber.child;
const nextFallbackChildSet = nextFiberChild
? nextFiberChild.sibling
: null;
if (nextFallbackChildSet != null) {
mountFiberRecursively(
nextFallbackChildSet,
shouldIncludeInTree ? nextFiber : parentFiber,
true,
traceNearestHostComponentUpdate,
);
shouldResetChildren = true;
}
} else {
// Common case: Primary -> Primary.
// This is the same code path as for non-Suspense fibers.
if (nextFiber.child !== prevFiber.child) {
// If the first child is different, we need to traverse them.
// Each next child will be either a new child (mount) or an alternate
(update).
let nextChild = nextFiber.child;
let prevChildAtSameIndex = prevFiber.child;
while (nextChild) {
// We already know children will be referentially different because
// they are either new mounts or alternates of previous children.
// Schedule updates and mounts depending on whether alternates exist.
// We don't track deletions here because they are reported separately.
if (nextChild.alternate) {
const prevChild = nextChild.alternate;
if (
updateFiberRecursively(
nextChild,
prevChild,
shouldIncludeInTree ? nextFiber : parentFiber,
traceNearestHostComponentUpdate,
)
) {
// If a nested tree child order changed but it can't handle its own
// child order invalidation (e.g. because it's filtered out like host
nodes),
// propagate the need to reset child order upwards to this Fiber.
shouldResetChildren = true;
}
// However we also keep track if the order of the children matches
// the previous order. They are always different referentially, but
// if the instances line up conceptually we'll want to know that.
if (prevChild !== prevChildAtSameIndex) {
shouldResetChildren = true;
}
} else {
mountFiberRecursively(
nextChild,
shouldIncludeInTree ? nextFiber : parentFiber,
false,
traceNearestHostComponentUpdate,
);
shouldResetChildren = true;
}
// Try the next child.
nextChild = nextChild.sibling;
// Advance the pointer in the previous list so that we can
// keep comparing if they line up.
if (!shouldResetChildren && prevChildAtSameIndex !== null) {
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
}
}
// If we have no more children, but used to, they don't line up.
if (prevChildAtSameIndex !== null) {
shouldResetChildren = true;
}
} else {
if (traceUpdatesEnabled) {
// If we're tracing updates and we've bailed out before reaching a host
node,
// we should fall back to recursively marking the nearest host
descendants for highlight.
if (traceNearestHostComponentUpdate) {
const hostFibers = findAllCurrentHostFibers(
getFiberIDThrows(nextFiber),
);
hostFibers.forEach(hostFiber => {
traceUpdatesForNodes.add(hostFiber.stateNode);
});
}
}
}
}
if (shouldIncludeInTree) {
const isProfilingSupported = nextFiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
recordProfilingDurations(nextFiber);
}
}
if (shouldResetChildren) {
// We need to crawl the subtree for closest non-filtered Fibers
// so that we can display them in a flat children set.
if (shouldIncludeInTree) {
// Normally, search for children from the rendered child.
let nextChildSet = nextFiber.child;
if (nextDidTimeOut) {
// Special case: timed-out Suspense renders the fallback set.
const nextFiberChild = nextFiber.child;
nextChildSet = nextFiberChild ? nextFiberChild.sibling : null;
}
if (nextChildSet != null) {
recordResetChildren(nextFiber, nextChildSet);
}
// We've handled the child order change for this Fiber.
// Since it's included, there's no need to invalidate parent child order.
return false;
} else {
// Let the closest unfiltered parent Fiber reset its child order instead.
return true;
}
} else {
return false;
}
}
function cleanup() {
// We don't patch any methods so there is no cleanup.
}
function flushInitialOperations() {
const localPendingOperationsQueue = pendingOperationsQueue;
pendingOperationsQueue = null;
if (
localPendingOperationsQueue !== null &&
localPendingOperationsQueue.length > 0
) {
// We may have already queued up some operations before the frontend
connected
// If so, let the frontend know about them.
localPendingOperationsQueue.forEach(operations => {
hook.emit('operations', operations);
});
} else {
// Before the traversals, remember to start tracking
// our path in case we have selection to restore.
if (trackedPath !== null) {
mightBeOnTrackedPath = true;
}
// If we have not been profiling, then we can just walk the tree and build up
its current state as-is.
hook.getFiberRoots(rendererID).forEach(root => {
currentRootID = getOrGenerateFiberID(root.current);
setRootPseudoKey(currentRootID, root.current);
currentRootID = getOrGenerateFiberID(current);
if (traceUpdatesEnabled) {
traceUpdatesForNodes.clear();
}
updaters: getUpdatersList(root),
if (alternate) {
// TODO: relying on this seems a bit fishy.
const wasMounted =
alternate.memoizedState != null &&
alternate.memoizedState.element != null &&
// A dehydrated root is not considered mounted
alternate.memoizedState.isDehydrated !== true;
const isMounted =
current.memoizedState != null &&
current.memoizedState.element != null &&
// A dehydrated root is not considered mounted
current.memoizedState.isDehydrated !== true;
if (!wasMounted && isMounted) {
// Mount a new root.
setRootPseudoKey(currentRootID, current);
mountFiberRecursively(current, null, false, false);
} else if (wasMounted && isMounted) {
// Update an existing root.
updateFiberRecursively(current, alternate, null, false);
} else if (wasMounted && !isMounted) {
// Unmount an existing root.
removeRootPseudoKey(currentRootID);
recordUnmount(current, false);
}
} else {
// Mount a new root.
setRootPseudoKey(currentRootID, current);
mountFiberRecursively(current, null, false, false);
}
if (commitProfilingMetadata != null) {
commitProfilingMetadata.push(
((currentCommitProfilingMetadata: any): CommitProfilingData),
);
} else {
((rootToCommitProfilingMetadataMap: any):
CommitProfilingMetadataMap).set(
currentRootID,
[((currentCommitProfilingMetadata: any): CommitProfilingData)],
);
}
}
}
if (traceUpdatesEnabled) {
hook.emit('traceUpdates', traceUpdatesForNodes);
}
currentRootID = -1;
}
function getFiberIDForNative(
hostInstance: NativeType,
findNearestUnfilteredAncestor: boolean = false,
) {
let fiber = renderer.findFiberByHostInstance(hostInstance);
if (fiber != null) {
if (findNearestUnfilteredAncestor) {
while (fiber !== null && shouldFilterFiber(fiber)) {
fiber = fiber.return;
}
}
return getFiberIDThrows(((fiber: any): Fiber));
}
return null;
}
// If both copies of the parent fiber point to the same child, we can
// assume that the child is current. This happens when we bailout on low
// priority: the bailed out fiber's child reuses the current child.
if (parentA.child === parentB.child) {
let child = parentA.child;
while (child) {
if (child === a) {
// We've determined that A is the current branch.
assertIsMounted(parentA);
return fiber;
}
if (child === b) {
// We've determined that B is the current branch.
assertIsMounted(parentA);
return alternate;
}
child = child.sibling;
}
// We should never have an alternate for any mounting node. So the only
// way this could possibly happen is if this was unmounted, if at all.
throw new Error('Unable to find node on an unmounted component.');
}
if (a.alternate !== b) {
throw new Error(
"Return fibers should always be each others' alternates. " +
'This error is likely caused by a bug in React. Please file an issue.',
);
}
}
if (a.stateNode.current === a) {
// We've determined that A is the current branch.
return fiber;
}
// Otherwise B has to be current branch.
return alternate;
}
function prepareViewAttributeSource(
id: number,
path: Array<string | number>,
): void {
if (isMostRecentlyInspectedElement(id)) {
window.$attribute = getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
);
}
}
switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
case IndeterminateComponent:
case FunctionComponent:
global.$type = type;
break;
case ForwardRef:
global.$type = type.render;
break;
case MemoComponent:
case SimpleMemoComponent:
global.$type =
elementType != null && elementType.type != null
? elementType.type
: type;
break;
default:
global.$type = null;
break;
}
}
if (_debugOwner) {
let owner: null | Fiber = _debugOwner;
while (owner !== null) {
owners.unshift(fiberToSerializedElement(owner));
owner = owner._debugOwner || null;
}
}
return owners;
}
switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
const instance = fiber.stateNode;
return (
typeof type.getDerivedStateFromError === 'function' ||
(instance !== null &&
typeof instance.componentDidCatch === 'function')
);
default:
return false;
}
}
const {
_debugOwner,
_debugSource,
stateNode,
key,
memoizedProps,
memoizedState,
dependencies,
tag,
type,
} = fiber;
const usesHooks =
(tag === FunctionComponent ||
tag === SimpleMemoComponent ||
tag === ForwardRef) &&
(!!memoizedState || !!dependencies);
if (!shouldHideContext) {
context = stateNode.context;
}
}
} else if (
typeSymbol === CONTEXT_NUMBER ||
typeSymbol === CONTEXT_SYMBOL_STRING
) {
// 16.3-16.5 read from "type" because the Consumer is the actual context
object.
// 16.6+ should read from "type._context" because Consumer can be different
(in DEV).
// NOTE Keep in sync with getDisplayNameForFiber()
const consumerResolvedContext = type._context || type;
current = current.return;
}
}
const isTimedOutSuspense =
tag === SuspenseComponent && memoizedState !== null;
try {
hooks = inspectHooksOfFiber(
fiber,
(renderer.currentDispatcherRef: any),
true, // Include source location info for hooks
);
} finally {
// Restore original console functionality.
for (const method in originalConsoleMethods) {
try {
// $FlowFixMe[prop-missing]
console[method] = originalConsoleMethods[method];
} catch (error) {}
}
}
}
if (enableStyleXFeatures) {
if (memoizedProps != null && memoizedProps.hasOwnProperty('xstyle')) {
plugins.stylex = getStyleXData(memoizedProps.xstyle);
}
}
return {
id,
// Does the current renderer support editable hooks and function props?
canEditHooks: typeof overrideHookState === 'function',
canEditFunctionProps: typeof overrideProps === 'function',
canToggleSuspense:
supportsTogglingSuspense &&
// If it's showing the real content, we can always flip fallback.
(!isTimedOutSuspense ||
// If it's showing fallback because we previously forced it to,
// allow toggling it back to remove the fallback override.
forceFallbackForSuspenseIDs.has(id)),
displayName: getDisplayNameForFiber(fiber),
type: elementType,
// Inspectable properties.
// TODO Review sanitization approach for the below inspectable values.
context,
hooks,
props: memoizedProps,
state: showState ? memoizedState : null,
errors: Array.from(errors.entries()),
warnings: Array.from(warnings.entries()),
// List of owners
owners,
rootType,
rendererPackageName: renderer.rendererPackageName,
rendererVersion: renderer.version,
plugins,
};
}
function createIsPathAllowed(
key: string | null,
secondaryCategory: 'hooks' | null,
) {
// This function helps prevent previously-inspected paths from being dehydrated
in updates.
// This is important to avoid a bad user experience where expanded toggles
collapse on update.
return function isPathAllowed(path: Array<string | number>): boolean {
switch (secondaryCategory) {
case 'hooks':
if (path.length === 1) {
// Never dehydrate the "hooks" object at the top levels.
return true;
}
if (
path[path.length - 2] === 'hookSource' &&
path[path.length - 1] === 'fileName'
) {
// It's important to preserve the full file name (URL) for hook sources
// in case the user has enabled the named hooks feature.
// Otherwise the frontend may end up with a partial URL which it can't
load.
return true;
}
if (
path[path.length - 1] === 'subHooks' ||
path[path.length - 2] === 'subHooks'
) {
// Dehydrating the 'subHooks' property makes the HooksTree UI a lot
more complicated,
// so it's easiest for now if we just don't break on this boundary.
// We can always dehydrate a level deeper (in the value object).
return true;
}
break;
default:
break;
}
let current =
key === null ? currentlyInspectedPaths : currentlyInspectedPaths[key];
if (!current) {
return false;
}
for (let i = 0; i < path.length; i++) {
current = current[path[i]];
if (!current) {
return false;
}
}
return true;
};
}
switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
case IndeterminateComponent:
global.$r = stateNode;
break;
case FunctionComponent:
global.$r = {
hooks,
props,
type,
};
break;
case ForwardRef:
global.$r = {
hooks,
props,
type: type.render,
};
break;
case MemoComponent:
case SimpleMemoComponent:
global.$r = {
hooks,
props,
type:
elementType != null && elementType.type != null
? elementType.type
: type,
};
break;
default:
global.$r = null;
break;
}
}
function storeAsGlobal(
id: number,
path: Array<string | number>,
count: number,
): void {
if (isMostRecentlyInspectedElement(id)) {
const value = getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
);
const key = `$reactTemp${count}`;
window[key] = value;
console.log(key);
console.log(value);
}
}
function getSerializedElementValueByPath(
id: number,
path: Array<string | number>,
): ?string {
if (isMostRecentlyInspectedElement(id)) {
const valueToCopy = getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
);
return serializeToString(valueToCopy);
}
}
function inspectElement(
requestID: number,
id: number,
path: Array<string | number> | null,
forceFullData: boolean,
): InspectedElementPayload {
if (path !== null) {
mergeInspectedPaths(path);
}
// If this element has not been updated since it was last inspected,
// we can just return the subset of data in the newly-inspected path.
return {
id,
responseID: requestID,
type: 'hydrated-path',
path,
value: cleanForBridge(
getInObject(
((mostRecentlyInspectedElement: any): InspectedElement),
path,
),
createIsPathAllowed(null, secondaryCategory),
path,
),
};
} else {
// If this element has not been updated since it was last inspected, we
don't need to return it.
// Instead we can just return the ID to indicate that it has not changed.
return {
id,
responseID: requestID,
type: 'no-change',
};
}
}
} else {
currentlyInspectedPaths = {};
}
hasElementUpdatedSinceLastInspected = false;
try {
mostRecentlyInspectedElement = inspectElementRaw(id);
} catch (error) {
// the error name is synced with ReactDebugHooks
if (error.name === 'ReactDebugToolsRenderError') {
let message = 'Error rendering inspected element.';
let stack;
// 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;
console.error(
'React DevTools encountered an error while trying to inspect hooks. ' +
'This is most likely caused by an error in current inspected
component' +
(componentName != null ? `: "${componentName}".` : '.') +
'\nThe error thrown in the component is: \n\n',
error.cause,
);
if (error.cause instanceof Error) {
message = error.cause.message || message;
stack = error.cause.stack;
}
}
return {
type: 'error',
errorType: 'user',
id,
responseID: requestID,
message,
stack,
};
}
return {
type: 'error',
errorType: 'uncaught',
id,
responseID: requestID,
message: error.message,
stack: error.stack,
};
}
return {
id,
responseID: requestID,
type: 'full-data',
// $FlowFixMe[prop-missing] found when upgrading Flow
value: cleanedInspectedElement,
};
}
function deletePath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
path: Array<string | number>,
): void {
const fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber !== null) {
const instance = fiber.stateNode;
switch (type) {
case 'context':
// To simplify hydration and display of primitive context values (e.g.
number, string)
// the inspectElement() method wraps context in a {value: ...} object.
// We need to remove the first part of the path (the "value") before
continuing.
path = path.slice(1);
switch (fiber.tag) {
case ClassComponent:
if (path.length === 0) {
// Simple context value (noop)
} else {
deletePathInObject(instance.context, path);
}
instance.forceUpdate();
break;
case FunctionComponent:
// Function components using legacy context are not editable
// because there's no instance on which to create a cloned, mutated
context.
break;
}
break;
case 'hooks':
if (typeof overrideHookStateDeletePath === 'function') {
overrideHookStateDeletePath(fiber, ((hookID: any): number), path);
}
break;
case 'props':
if (instance === null) {
if (typeof overridePropsDeletePath === 'function') {
overridePropsDeletePath(fiber, path);
}
} else {
fiber.pendingProps = copyWithDelete(instance.props, path);
instance.forceUpdate();
}
break;
case 'state':
deletePathInObject(instance.state, path);
instance.forceUpdate();
break;
}
}
}
function renamePath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
oldPath: Array<string | number>,
newPath: Array<string | number>,
): void {
const fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber !== null) {
const instance = fiber.stateNode;
switch (type) {
case 'context':
// To simplify hydration and display of primitive context values (e.g.
number, string)
// the inspectElement() method wraps context in a {value: ...} object.
// We need to remove the first part of the path (the "value") before
continuing.
oldPath = oldPath.slice(1);
newPath = newPath.slice(1);
switch (fiber.tag) {
case ClassComponent:
if (oldPath.length === 0) {
// Simple context value (noop)
} else {
renamePathInObject(instance.context, oldPath, newPath);
}
instance.forceUpdate();
break;
case FunctionComponent:
// Function components using legacy context are not editable
// because there's no instance on which to create a cloned, mutated
context.
break;
}
break;
case 'hooks':
if (typeof overrideHookStateRenamePath === 'function') {
overrideHookStateRenamePath(
fiber,
((hookID: any): number),
oldPath,
newPath,
);
}
break;
case 'props':
if (instance === null) {
if (typeof overridePropsRenamePath === 'function') {
overridePropsRenamePath(fiber, oldPath, newPath);
}
} else {
fiber.pendingProps = copyWithRename(
instance.props,
oldPath,
newPath,
);
instance.forceUpdate();
}
break;
case 'state':
renamePathInObject(instance.state, oldPath, newPath);
instance.forceUpdate();
break;
}
}
}
function overrideValueAtPath(
type: 'context' | 'hooks' | 'props' | 'state',
id: number,
hookID: ?number,
path: Array<string | number>,
value: any,
): void {
const fiber = findCurrentFiberUsingSlowPathById(id);
if (fiber !== null) {
const instance = fiber.stateNode;
switch (type) {
case 'context':
// To simplify hydration and display of primitive context values (e.g.
number, string)
// the inspectElement() method wraps context in a {value: ...} object.
// We need to remove the first part of the path (the "value") before
continuing.
path = path.slice(1);
switch (fiber.tag) {
case ClassComponent:
if (path.length === 0) {
// Simple context value
instance.context = value;
} else {
setInObject(instance.context, path, value);
}
instance.forceUpdate();
break;
case FunctionComponent:
// Function components using legacy context are not editable
// because there's no instance on which to create a cloned, mutated
context.
break;
}
break;
case 'hooks':
if (typeof overrideHookState === 'function') {
overrideHookState(fiber, ((hookID: any): number), path, value);
}
break;
case 'props':
switch (fiber.tag) {
case ClassComponent:
fiber.pendingProps = copyWithSet(instance.props, path, value);
instance.forceUpdate();
break;
default:
if (typeof overrideProps === 'function') {
overrideProps(fiber, path, value);
}
break;
}
break;
case 'state':
switch (fiber.tag) {
case ClassComponent:
setInObject(instance.state, path, value);
instance.forceUpdate();
break;
}
break;
}
}
}
type CommitProfilingData = {
changeDescriptions: Map<number, ChangeDescription> | null,
commitTime: number,
durations: Array<number>,
effectDuration: number | null,
maxActualDuration: number,
passiveEffectDuration: number | null,
priorityLevel: string | null,
updaters: Array<SerializedElement> | null,
};
rootToCommitProfilingMetadataMap.forEach(
(commitProfilingMetadata, rootID) => {
const commitData: Array<CommitDataBackend> = [];
const initialTreeBaseDurations: Array<[number, number]> = [];
const displayName =
(displayNamesByRootID !== null && displayNamesByRootID.get(rootID)) ||
'Unknown';
if (initialTreeBaseDurationsMap != null) {
initialTreeBaseDurationsMap.forEach((treeBaseDuration, id) => {
if (
initialIDToRootMap != null &&
initialIDToRootMap.get(id) === rootID
) {
// We don't need to convert milliseconds to microseconds in this
case,
// because the profiling summary is JSON serialized.
initialTreeBaseDurations.push([id, treeBaseDuration]);
}
});
}
commitData.push({
changeDescriptions:
changeDescriptions !== null
? Array.from(changeDescriptions.entries())
: null,
duration: maxActualDuration,
effectDuration,
fiberActualDurations,
fiberSelfDurations,
passiveEffectDuration,
priorityLevel,
timestamp: commitTime,
updaters,
});
});
dataForRoots.push({
commitData,
displayName,
initialTreeBaseDurations,
rootID,
});
},
);
timelineData = {
...rest,
return {
dataForRoots,
rendererID,
timelineData,
};
}
recordChangeDescriptions = shouldRecordChangeDescriptions;
hook.getFiberRoots(rendererID).forEach(root => {
const rootID = getFiberIDThrows(root.current);
((displayNamesByRootID: any): DisplayNamesByRootID).set(
rootID,
getDisplayNameForRoot(root.current),
);
if (shouldRecordChangeDescriptions) {
// Record all contexts at the time profiling is started.
// Fibers only store the current context value,
// so we need to track them separately in order to determine changed keys.
crawlToInitializeContextsMap(root.current);
}
});
isProfiling = true;
profilingStartTime = getCurrentTime();
rootToCommitProfilingMetadataMap = new Map();
function stopProfiling() {
isProfiling = false;
recordChangeDescriptions = false;
// Automatically start profiling so that we don't miss timing info from initial
"mount".
if (
sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true'
) {
startProfiling(
sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) ===
'true',
);
}
// React will switch between these implementations depending on whether
// we have any manually suspended/errored-out Fibers or not.
function shouldErrorFiberAlwaysNull() {
return null;
}
// Map of id and its force error status: true (error), false (toggled off),
// null (do nothing)
const forceErrorForFiberIDs = new Map<number | null, $FlowFixMe>();
const id = getFiberIDUnsafe(fiber);
if (id === null) {
return null;
}
if (forceErrorForFiberIDs.size === 0) {
// Last override is gone. Switch React back to fast path.
setErrorHandler(shouldErrorFiberAlwaysNull);
}
}
}
return status;
}
if (forceErrorForFiberIDs.size === 1) {
// First override is added. Switch React to slower path.
setErrorHandler(shouldErrorFiberAccordingToMap);
}
function shouldSuspendFiberAlwaysFalse() {
return false;
}
function updateTrackedPathStateAfterMount(
mightSiblingsBeOnTrackedPath: boolean,
) {
// updateTrackedPathStateBeforeMount() told us whether to match siblings.
// Now that we're entering siblings, let's use that information.
mightBeOnTrackedPath = mightSiblingsBeOnTrackedPath;
}
switch (priorityLevel) {
case ImmediatePriority:
return 'Immediate';
case UserBlockingPriority:
return 'User-Blocking';
case NormalPriority:
return 'Normal';
case LowPriority:
return 'Low';
case IdlePriority:
return 'Idle';
case NoPriority:
default:
return 'Unknown';
}
};
return {
cleanup,
clearErrorsAndWarnings,
clearErrorsForFiberID,
clearWarningsForFiberID,
getSerializedElementValueByPath,
deletePath,
findNativeNodesForFiberID,
flushInitialOperations,
getBestMatchForTrackedPath,
getDisplayNameForFiberID,
getFiberForNative,
getFiberIDForNative,
getInstanceAndStyle,
getOwnersList,
getPathForElement,
getProfilingData,
handleCommitFiberRoot,
handleCommitFiberUnmount,
handlePostCommitFiberRoot,
hasFiberWithId,
inspectElement,
logElementToConsole,
patchConsoleForStrictMode,
prepareViewAttributeSource,
prepareViewElementSource,
overrideError,
overrideSuspense,
overrideValueAtPath,
renamePath,
renderer,
setTraceUpdatesEnabled,
setTrackedPath,
startProfiling,
stopProfiling,
storeAsGlobal,
unpatchConsoleForStrictMode,
updateComponentFilters,
};
}