Compare commits

...

4 Commits

Author SHA1 Message Date
Mofei Zhang
e2c0670eb2 [DevTools] Track all public HostInstances in a Map (#30831)
This lets us get from a HostInstance to the nearest DevToolsInstance
without relying on `findFiberByHostInstance` and
`fiberToDevToolsInstanceMap`. We already did the equivalent of this for
Resources in HostHoistables.

One issue before was that we'd ideally get away from the
`fiberToDevToolsInstanceMap` map in general since we should ideally not
treat Fibers as stateful but they could be replaced by something else
stateful in principle.

This PR also addresses Virtual Instances. Now you can select a DOM node
and have it select a Virtual Instance if that's the nearest parent since
the parent doesn't have to be a Fiber anymore.

However, the other reason for this change is that I'd like to get rid of
the need for the `findFiberByHostInstance` from being injected. A
renderer should not need to store a reference back from its instance to
a Fiber. Without the Synthetic Event system this wouldn't be needed by
the renderer so we should be able to remove it. We also don't really
need it since we have all the information by just walking the commit to
collect the nodes if we just maintain our own Map.

There's one subtle nuance that the different renderers do. Typically a
HostInstance is the same thing as a PublicInstance in React but
technically in Fabric they're not the same. So we need to translate
between PublicInstance and HostInstance. I just hardcoded the Fabric
implementation of this since it's the only known one that does this but
could feature detect other ones too if necessary. On one hand it's more
resilient to refactors to not rely on injected helpers and on hand it
doesn't follow changes to things like this.

For the conflict resolution I added in #30494 I had to make that
specific to DOM so we can move the DOM traversal to the backend instead
of the injected helper.

[ghstack-poisoned]
2024-10-11 16:21:56 -04:00
Mofei Zhang
02732c11c6 [Fiber] Stash ThenableState on the Dependencies Object for Use By DevTools (#30866)
This lets us track what a Component might suspend on from DevTools. We
could already collect this by replaying a component's Hooks but that
would be expensive to collect from a whole tree.

The thenables themselves might contain useful information but mainly
we'd want access to the `_debugInfo` on the thenables which might
contain additional information from the server.


19bd26beb6/packages/shared/ReactTypes.js (L114)

In a follow up we should really do something similar in Flight to
transfer `use()` on the debugInfo of that Server Component.

[ghstack-poisoned]
2024-10-11 16:21:47 -04:00
Mofei Zhang
9af74e024c [DevTools] Support VirtualInstances in findAllCurrentHostInstances (#30853)
This lets us highlight Server Components.

However, there is a problem with this because if the actual nearest
Fiber is filtered, there's no FiberInstance and so we might skip past it
and maybe never find a child while walking the whole tree. This is very
common in the case where you have just Server Components and Host
Components which are filtered by default.

Note how the DOM nodes that are just plain host instances without client
component wrappers are not highlighted here:

<img width="1102" alt="Screenshot 2024-08-30 at 4 33 55 PM"
src="https://github.com/user-attachments/assets/c9a7b91e-5faf-4c60-99a8-1195539ff8b5">

Fixing that needs a separate refactor though and related to several
other features that already have a similar issue without
VirtualInstances like Suspense/Error Boundaries (triggering
suspense/error on a filtered Suspense/ErrorBoundary doesn't work
correctly). So this first PR just adds the feature for the common case
where there's at least some Fibers.

[ghstack-poisoned]
2024-10-11 16:21:39 -04:00
Mofei Zhang
b658948159 [DevTools] Add Filtering of Environment Names (#30850)
Stacked on #30842.

This adds a filter to be able to exclude Components from a certain
environment. Default to Client or Server.

The available options are computed into a dropdown based on the names
that are currently used on the page (or an option that were previously
used). In addition to the hardcoded "Client". Meaning that if you have
Server Components on the page you see "Server" or "Client" as possible
options but it can be anything if there are multiple RSC environments on
the page.

"Client" in this case means Function and Class Components in Fiber -
excluding built-ins.

If a Server Component has two environments (primary and secondary) then
both have to be filtered to exclude it.

We don't show the option at all if there are no Server Components used
in the page to avoid confusing existing users that are just using Client
Components and wouldn't know the difference between Server vs Client.

<img width="815" alt="Screenshot 2024-08-30 at 12 56 42 AM"
src="https://github.com/user-attachments/assets/e06b225a-e85d-4cdc-8707-d4630fede19e">

[ghstack-poisoned]
2024-10-11 16:21:29 -04:00
16 changed files with 636 additions and 300 deletions

View File

@@ -284,6 +284,19 @@ export function createHOCFilter(isEnabled: boolean = true) {
};
}
export function createEnvironmentNameFilter(
env: string,
isEnabled: boolean = true,
) {
const Types = require('react-devtools-shared/src/frontend/types');
return {
type: Types.ComponentFilterEnvironmentName,
isEnabled,
isValid: true,
value: env,
};
}
export function createElementTypeFilter(
elementType: ElementType,
isEnabled: boolean = true,

View File

@@ -220,6 +220,7 @@ export default class Agent extends EventEmitter<{
this.updateConsolePatchSettings,
);
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
bridge.addListener('getEnvironmentNames', this.getEnvironmentNames);
// Temporarily support older standalone front-ends sending commands to newer embedded backends.
// We do this because React Native embeds the React DevTools backend,
@@ -341,84 +342,123 @@ export default class Agent extends EventEmitter<{
}
getIDForHostInstance(target: HostInstance): number | null {
let bestMatch: null | HostInstance = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null = renderer.getNearestMountedHostInstance(target);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (
bestMatch === null ||
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const match = renderer.getElementIDForHostInstance(target);
if (match != null) {
return match;
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
return bestRenderer.getElementIDForHostInstance(bestMatch, true);
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
return bestRenderer.getElementIDForHostInstance(bestMatch);
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
}
return null;
}
getComponentNameForHostInstance(target: HostInstance): string | null {
// We duplicate this code from getIDForHostInstance to avoid an object allocation.
let bestMatch: null | HostInstance = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode = renderer.getNearestMountedHostInstance(target);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (
bestMatch === null ||
(!isReactNativeEnvironment() && bestMatch.contains(nearestNode))
) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
if (isReactNativeEnvironment() || typeof target.nodeType !== 'number') {
// In React Native or non-DOM we simply pick any renderer that has a match.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
try {
const id = renderer.getElementIDForHostInstance(target);
if (id) {
return renderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch, true);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
return null;
} else {
// In the DOM we use a smarter mechanism to find the deepest a DOM node
// that is registered if there isn't an exact match.
let bestMatch: null | Element = null;
let bestRenderer: null | RendererInterface = null;
// Find the nearest ancestor which is mounted by a React.
for (const rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
const nearestNode: null | Element = renderer.getNearestMountedDOMNode(
(target: any),
);
if (nearestNode !== null) {
if (nearestNode === target) {
// Exact match we can exit early.
bestMatch = nearestNode;
bestRenderer = renderer;
break;
}
if (bestMatch === null || bestMatch.contains(nearestNode)) {
// If this is the first match or the previous match contains the new match,
// so the new match is a deeper and therefore better match.
bestMatch = nearestNode;
bestRenderer = renderer;
}
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
if (bestRenderer != null && bestMatch != null) {
try {
const id = bestRenderer.getElementIDForHostInstance(bestMatch);
if (id) {
return bestRenderer.getDisplayNameForElementID(id);
}
} catch (error) {
// Some old React versions might throw if they can't find a match.
// If so we should ignore it...
}
}
return null;
}
return null;
}
getBackendVersion: () => void = () => {
@@ -814,6 +854,24 @@ export default class Agent extends EventEmitter<{
}
};
getEnvironmentNames: () => void = () => {
let accumulatedNames = null;
for (const rendererID in this._rendererInterfaces) {
const renderer = this._rendererInterfaces[+rendererID];
const names = renderer.getEnvironmentNames();
if (accumulatedNames === null) {
accumulatedNames = names;
} else {
for (let i = 0; i < names.length; i++) {
if (accumulatedNames.indexOf(names[i]) === -1) {
accumulatedNames.push(names[i]);
}
}
}
}
this._bridge.send('environmentNames', accumulatedNames || []);
};
onTraceUpdates: (nodes: Set<HostInstance>) => void = nodes => {
this.emit('traceUpdates', nodes);
};

View File

@@ -135,17 +135,7 @@ export function registerRenderer(
renderer: ReactRenderer,
onErrorOrWarning?: OnErrorOrWarning,
): void {
const {
currentDispatcherRef,
getCurrentFiber,
findFiberByHostInstance,
version,
} = renderer;
// Ignore React v15 and older because they don't expose a component stack anyway.
if (typeof findFiberByHostInstance !== 'function') {
return;
}
const {currentDispatcherRef, getCurrentFiber, version} = renderer;
// currentDispatcherRef gets injected for v16.8+ to support hooks inspection.
// getCurrentFiber gets injected for v16.9+.

View File

@@ -14,6 +14,7 @@ import {
ComponentFilterElementType,
ComponentFilterHOC,
ComponentFilterLocation,
ComponentFilterEnvironmentName,
ElementTypeClass,
ElementTypeContext,
ElementTypeFunction,
@@ -721,6 +722,11 @@ export function getInternalReactConstants(version: string): {
};
}
// All environment names we've seen so far. This lets us create a list of filters to apply.
// This should ideally include env of filtered Components too so that you can add those as
// filters at the same time as removing some other filter.
const knownEnvironmentNames: Set<string> = new Set();
// Map of one or more Fibers in a pair to their unique id number.
// We track both Fibers to support Fast Refresh,
// which may forcefully replace one of the pair as part of hot reloading.
@@ -732,35 +738,93 @@ const fiberToFiberInstanceMap: Map<Fiber, FiberInstance> = new Map();
// operations that should be the same whether the current and work-in-progress Fiber is used.
const idToDevToolsInstanceMap: Map<number, DevToolsInstance> = new Map();
// Map of resource DOM nodes to all the Fibers that depend on it.
const hostResourceToFiberMap: Map<HostInstance, Set<Fiber>> = new Map();
// Map of canonical HostInstances to the nearest parent DevToolsInstance.
const publicInstanceToDevToolsInstanceMap: Map<HostInstance, DevToolsInstance> =
new Map();
// Map of resource DOM nodes to all the nearest DevToolsInstances that depend on it.
const hostResourceToDevToolsInstanceMap: Map<
HostInstance,
Set<DevToolsInstance>,
> = new Map();
function getPublicInstance(instance: HostInstance): HostInstance {
// Typically the PublicInstance and HostInstance is the same thing but not in Fabric.
// So we need to detect this and use that as the public instance.
return typeof instance === 'object' &&
instance !== null &&
typeof instance.canonical === 'object'
? (instance.canonical: any)
: typeof instance._nativeTag === 'number'
? instance._nativeTag
: instance;
}
function aquireHostInstance(
nearestInstance: DevToolsInstance,
hostInstance: HostInstance,
): void {
const publicInstance = getPublicInstance(hostInstance);
publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);
}
function releaseHostInstance(
nearestInstance: DevToolsInstance,
hostInstance: HostInstance,
): void {
const publicInstance = getPublicInstance(hostInstance);
if (
publicInstanceToDevToolsInstanceMap.get(publicInstance) === nearestInstance
) {
publicInstanceToDevToolsInstanceMap.delete(publicInstance);
}
}
function aquireHostResource(
fiber: Fiber,
nearestInstance: DevToolsInstance,
resource: ?{instance?: HostInstance},
): void {
const hostInstance = resource && resource.instance;
if (hostInstance) {
let resourceFibers = hostResourceToFiberMap.get(hostInstance);
if (resourceFibers === undefined) {
resourceFibers = new Set();
hostResourceToFiberMap.set(hostInstance, resourceFibers);
const publicInstance = getPublicInstance(hostInstance);
let resourceInstances =
hostResourceToDevToolsInstanceMap.get(publicInstance);
if (resourceInstances === undefined) {
resourceInstances = new Set();
hostResourceToDevToolsInstanceMap.set(publicInstance, resourceInstances);
// Store the first match in the main map for quick access when selecting DOM node.
publicInstanceToDevToolsInstanceMap.set(publicInstance, nearestInstance);
}
resourceFibers.add(fiber);
resourceInstances.add(nearestInstance);
}
}
function releaseHostResource(
fiber: Fiber,
nearestInstance: DevToolsInstance,
resource: ?{instance?: HostInstance},
): void {
const hostInstance = resource && resource.instance;
if (hostInstance) {
const resourceFibers = hostResourceToFiberMap.get(hostInstance);
if (resourceFibers !== undefined) {
resourceFibers.delete(fiber);
if (resourceFibers.size === 0) {
hostResourceToFiberMap.delete(hostInstance);
const publicInstance = getPublicInstance(hostInstance);
const resourceInstances =
hostResourceToDevToolsInstanceMap.get(publicInstance);
if (resourceInstances !== undefined) {
resourceInstances.delete(nearestInstance);
if (resourceInstances.size === 0) {
hostResourceToDevToolsInstanceMap.delete(publicInstance);
publicInstanceToDevToolsInstanceMap.delete(publicInstance);
} else if (
publicInstanceToDevToolsInstanceMap.get(publicInstance) ===
nearestInstance
) {
// This was the first one. Store the next first one in the main map for easy access.
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const firstInstance of resourceInstances) {
publicInstanceToDevToolsInstanceMap.set(
firstInstance,
nearestInstance,
);
break;
}
}
}
}
@@ -1099,6 +1163,7 @@ export function attach(
const hideElementsWithDisplayNames: Set<RegExp> = new Set();
const hideElementsWithPaths: Set<RegExp> = new Set();
const hideElementsWithTypes: Set<ElementType> = new Set();
const hideElementsWithEnvs: Set<string> = new Set();
// Highlight updates
let traceUpdatesEnabled: boolean = false;
@@ -1108,6 +1173,7 @@ export function attach(
hideElementsWithTypes.clear();
hideElementsWithDisplayNames.clear();
hideElementsWithPaths.clear();
hideElementsWithEnvs.clear();
componentFilters.forEach(componentFilter => {
if (!componentFilter.isEnabled) {
@@ -1133,6 +1199,9 @@ export function attach(
case ComponentFilterHOC:
hideElementsWithDisplayNames.add(new RegExp('\\('));
break;
case ComponentFilterEnvironmentName:
hideElementsWithEnvs.add(componentFilter.value);
break;
default:
console.warn(
`Invalid component filter type "${componentFilter.type}"`,
@@ -1215,7 +1284,14 @@ export function attach(
flushPendingEvents();
}
function shouldFilterVirtual(data: ReactComponentInfo): boolean {
function getEnvironmentNames(): Array<string> {
return Array.from(knownEnvironmentNames);
}
function shouldFilterVirtual(
data: ReactComponentInfo,
secondaryEnv: null | string,
): boolean {
// For purposes of filtering Server Components are always Function Components.
// Environment will be used to filter Server vs Client.
// Technically they can be forwardRef and memo too but those filters will go away
@@ -1236,6 +1312,14 @@ export function attach(
}
}
if (
(data.env == null || hideElementsWithEnvs.has(data.env)) &&
(secondaryEnv === null || hideElementsWithEnvs.has(secondaryEnv))
) {
// If a Component has two environments, you have to filter both for it not to appear.
return true;
}
return false;
}
@@ -1294,6 +1378,26 @@ export function attach(
}
}
if (hideElementsWithEnvs.has('Client')) {
// If we're filtering out the Client environment we should filter out all
// "Client Components". Technically that also includes the built-ins but
// since that doesn't actually include any additional code loading it's
// useful to not filter out the built-ins. Those can be filtered separately.
// There's no other way to filter out just Function components on the Client.
// Therefore, this only filters Class and Function components.
switch (tag) {
case ClassComponent:
case IncompleteClassComponent:
case IncompleteFunctionComponent:
case FunctionComponent:
case IndeterminateComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
return true;
}
}
/* DISABLED: https://github.com/facebook/react/pull/28417
if (hideElementsWithPaths.size > 0) {
const source = getSourceForFiber(fiber);
@@ -1421,50 +1525,29 @@ export function attach(
// 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 untrackFiber(fiberInstance: FiberInstance) {
function untrackFiber(nearestInstance: DevToolsInstance, fiber: Fiber) {
if (__DEBUG__) {
debug('untrackFiber()', fiberInstance.data, null);
debug('untrackFiber()', fiber, null);
}
// TODO: Consider using a WeakMap instead. The only thing where that doesn't work
// is React Native Paper which tracks tags but that support is eventually going away
// and can use the old findFiberByHostInstance strategy.
if (fiber.tag === HostHoistable) {
releaseHostResource(nearestInstance, fiber.memoizedState);
} else if (
fiber.tag === HostComponent ||
fiber.tag === HostText ||
fiber.tag === HostSingleton
) {
releaseHostInstance(nearestInstance, fiber.stateNode);
}
idToDevToolsInstanceMap.delete(fiberInstance.id);
const fiber = fiberInstance.data;
// Restore any errors/warnings associated with this fiber to the pending
// map. I.e. treat it as before we tracked the instances. This lets us
// restore them if we remount the same Fibers later. Otherwise we rely
// on the GC of the Fibers to clean them up.
if (fiberInstance.errors !== null) {
pendingFiberToErrorsMap.set(fiber, fiberInstance.errors);
fiberInstance.errors = null;
}
if (fiberInstance.warnings !== null) {
pendingFiberToWarningsMap.set(fiber, fiberInstance.warnings);
fiberInstance.warnings = null;
}
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);
}
}
if (fiberToFiberInstanceMap.get(fiber) === fiberInstance) {
fiberToFiberInstanceMap.delete(fiber);
}
const {alternate} = fiber;
if (alternate !== null) {
if (fiberToFiberInstanceMap.get(alternate) === fiberInstance) {
fiberToFiberInstanceMap.delete(alternate);
// Recursively clean up any filtered Fibers below this one as well since
// we won't recordUnmount on those.
for (let child = fiber.child; child !== null; child = child.sibling) {
if (shouldFilterFiber(child)) {
untrackFiber(nearestInstance, child);
}
}
}
@@ -2309,7 +2392,47 @@ export function attach(
pendingRealUnmountedIDs.push(id);
}
untrackFiber(fiberInstance);
idToDevToolsInstanceMap.delete(fiberInstance.id);
// Restore any errors/warnings associated with this fiber to the pending
// map. I.e. treat it as before we tracked the instances. This lets us
// restore them if we remount the same Fibers later. Otherwise we rely
// on the GC of the Fibers to clean them up.
if (fiberInstance.errors !== null) {
pendingFiberToErrorsMap.set(fiber, fiberInstance.errors);
fiberInstance.errors = null;
}
if (fiberInstance.warnings !== null) {
pendingFiberToWarningsMap.set(fiber, fiberInstance.warnings);
fiberInstance.warnings = null;
}
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);
}
}
if (fiberToFiberInstanceMap.get(fiber) === fiberInstance) {
fiberToFiberInstanceMap.delete(fiber);
}
const {alternate} = fiber;
if (alternate !== null) {
if (fiberToFiberInstanceMap.get(alternate) === fiberInstance) {
fiberToFiberInstanceMap.delete(alternate);
}
}
untrackFiber(fiberInstance, fiber);
}
// Running state of the remaining children from the previous version of this parent that
@@ -2489,7 +2612,14 @@ export function attach(
}
// Scan up until the next Component to see if this component changed environment.
const componentInfo: ReactComponentInfo = (debugEntry: any);
if (shouldFilterVirtual(componentInfo)) {
const secondaryEnv = getSecondaryEnvironmentName(fiber._debugInfo, i);
if (componentInfo.env != null) {
knownEnvironmentNames.add(componentInfo.env);
}
if (secondaryEnv !== null) {
knownEnvironmentNames.add(secondaryEnv);
}
if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
// Skip.
continue;
}
@@ -2511,10 +2641,6 @@ export function attach(
);
}
previousVirtualInstance = createVirtualInstance(componentInfo);
const secondaryEnv = getSecondaryEnvironmentName(
fiber._debugInfo,
i,
);
recordVirtualMount(
previousVirtualInstance,
reconcilingParent,
@@ -2621,7 +2747,21 @@ export function attach(
}
if (fiber.tag === HostHoistable) {
aquireHostResource(fiber, fiber.memoizedState);
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
aquireHostResource(nearestInstance, fiber.memoizedState);
} else if (
fiber.tag === HostComponent ||
fiber.tag === HostText ||
fiber.tag === HostSingleton
) {
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
aquireHostInstance(nearestInstance, fiber.stateNode);
}
if (fiber.tag === SuspenseComponent) {
@@ -2919,7 +3059,17 @@ export function attach(
continue;
}
const componentInfo: ReactComponentInfo = (debugEntry: any);
if (shouldFilterVirtual(componentInfo)) {
const secondaryEnv = getSecondaryEnvironmentName(
nextChild._debugInfo,
i,
);
if (componentInfo.env != null) {
knownEnvironmentNames.add(componentInfo.env);
}
if (secondaryEnv !== null) {
knownEnvironmentNames.add(secondaryEnv);
}
if (shouldFilterVirtual(componentInfo, secondaryEnv)) {
continue;
}
if (level === virtualLevel) {
@@ -2983,10 +3133,6 @@ export function attach(
} else {
// Otherwise we create a new instance.
const newVirtualInstance = createVirtualInstance(componentInfo);
const secondaryEnv = getSecondaryEnvironmentName(
nextChild._debugInfo,
i,
);
recordVirtualMount(
newVirtualInstance,
reconcilingParent,
@@ -3236,8 +3382,12 @@ export function attach(
}
try {
if (nextFiber.tag === HostHoistable) {
releaseHostResource(prevFiber, prevFiber.memoizedState);
aquireHostResource(nextFiber, nextFiber.memoizedState);
const nearestInstance = reconcilingParent;
if (nearestInstance === null) {
throw new Error('Did not expect a host hoistable to be the root');
}
releaseHostResource(nearestInstance, prevFiber.memoizedState);
aquireHostResource(nearestInstance, nextFiber.memoizedState);
}
const isSuspense = nextFiber.tag === SuspenseComponent;
@@ -3338,6 +3488,18 @@ export function attach(
// I.e. we just restore them by undoing what we did above.
fiberInstance.firstChild = remainingReconcilingChildren;
remainingReconcilingChildren = null;
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 hostInstances =
findAllCurrentHostInstances(fiberInstance);
hostInstances.forEach(hostInstance => {
traceUpdatesForNodes.add(hostInstance);
});
}
}
} else {
// If this fiber is filtered there might be changes to this set elsewhere so we have
// to visit each child to place it back in the set. We let the child bail out instead.
@@ -3349,19 +3511,6 @@ export function attach(
);
}
}
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 hostInstances = findAllCurrentHostInstances(
getFiberInstanceThrows(nextFiber),
);
hostInstances.forEach(hostInstance => {
traceUpdatesForNodes.add(hostInstance);
});
}
}
}
}
@@ -3635,15 +3784,31 @@ export function attach(
return null;
}
function findAllCurrentHostInstances(
fiberInstance: FiberInstance,
): $ReadOnlyArray<HostInstance> {
const hostInstances = [];
const fiber = fiberInstance.data;
if (!fiber) {
return hostInstances;
function appendHostInstancesByDevToolsInstance(
devtoolsInstance: DevToolsInstance,
hostInstances: Array<HostInstance>,
) {
if (devtoolsInstance.kind === FIBER_INSTANCE) {
const fiber = devtoolsInstance.data;
appendHostInstancesByFiber(fiber, hostInstances);
return;
}
// Search the tree for the nearest child Fiber and add all its host instances.
// TODO: If the true nearest Fiber is filtered, we might skip it and instead include all
// the children below it. In the extreme case, searching the whole tree.
for (
let child = devtoolsInstance.firstChild;
child !== null;
child = child.nextSibling
) {
appendHostInstancesByDevToolsInstance(child, hostInstances);
}
}
function appendHostInstancesByFiber(
fiber: Fiber,
hostInstances: Array<HostInstance>,
): void {
// Next we'll drill down this component to find all HostComponent/Text.
let node: Fiber = fiber;
while (true) {
@@ -3663,19 +3828,24 @@ export function attach(
continue;
}
if (node === fiber) {
return hostInstances;
return;
}
while (!node.sibling) {
if (!node.return || node.return === fiber) {
return hostInstances;
return;
}
node = node.return;
}
node.sibling.return = node.return;
node = node.sibling;
}
// Flow needs the return here, but ESLint complains about it.
// eslint-disable-next-line no-unreachable
}
function findAllCurrentHostInstances(
devtoolsInstance: DevToolsInstance,
): $ReadOnlyArray<HostInstance> {
const hostInstances: Array<HostInstance> = [];
appendHostInstancesByDevToolsInstance(devtoolsInstance, hostInstances);
return hostInstances;
}
@@ -3686,17 +3856,7 @@ export function attach(
console.warn(`Could not find DevToolsInstance with id "${id}"`);
return null;
}
if (devtoolsInstance.kind !== FIBER_INSTANCE) {
// TODO: Handle VirtualInstance.
return null;
}
const fiber = devtoolsInstance.data;
if (fiber === null) {
return null;
}
const hostInstances = findAllCurrentHostInstances(devtoolsInstance);
return hostInstances;
return findAllCurrentHostInstances(devtoolsInstance);
} catch (err) {
// The fiber might have unmounted by now.
return null;
@@ -3715,82 +3875,21 @@ export function attach(
}
}
function getNearestMountedHostInstance(
hostInstance: HostInstance,
): null | HostInstance {
const mountedFiber = renderer.findFiberByHostInstance(hostInstance);
if (mountedFiber != null) {
if (mountedFiber.stateNode !== hostInstance) {
// If it's not a perfect match the specific one might be a resource.
// We don't need to look at any parents because host resources don't have
// children so it won't be in any parent if it's not this one.
if (hostResourceToFiberMap.has(hostInstance)) {
return hostInstance;
}
}
return mountedFiber.stateNode;
function getNearestMountedDOMNode(publicInstance: Element): null | Element {
let domNode: null | Element = publicInstance;
while (domNode && !publicInstanceToDevToolsInstanceMap.has(domNode)) {
// $FlowFixMe: In practice this is either null or Element.
domNode = domNode.parentNode;
}
if (hostResourceToFiberMap.has(hostInstance)) {
return hostInstance;
}
return null;
}
function findNearestUnfilteredElementID(searchFiber: Fiber) {
let fiber: null | Fiber = searchFiber;
while (fiber !== null) {
const fiberInstance = getFiberInstanceUnsafe(fiber);
if (fiberInstance !== null) {
// TODO: Ideally we would not have any filtered FiberInstances which
// would make this logic much simpler. Unfortunately, we sometimes
// eagerly add to the map and some times don't eagerly clean it up.
// TODO: If the fiber is filtered, the FiberInstance wouldn't really
// exist which would mean that we also don't have a way to get to the
// VirtualInstances.
if (!shouldFilterFiber(fiberInstance.data)) {
return fiberInstance.id;
}
// We couldn't use this Fiber but we might have a VirtualInstance
// that is the nearest unfiltered instance.
const parentInstance = fiberInstance.parent;
if (
parentInstance !== null &&
parentInstance.kind === VIRTUAL_INSTANCE
) {
// Virtual Instances only exist if they're unfiltered.
return parentInstance.id;
}
// If we find a parent Fiber, it might not be the nearest parent
// so we break out and continue walking the Fiber tree instead.
}
fiber = fiber.return;
}
return null;
return domNode;
}
function getElementIDForHostInstance(
hostInstance: HostInstance,
findNearestUnfilteredAncestor: boolean = false,
publicInstance: HostInstance,
): number | null {
const resourceFibers = hostResourceToFiberMap.get(hostInstance);
if (resourceFibers !== undefined) {
// This is a resource. Find the first unfiltered instance.
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const resourceFiber of resourceFibers) {
const elementID = findNearestUnfilteredElementID(resourceFiber);
if (elementID !== null) {
return elementID;
}
}
// If we don't find one, fallthrough to select the parent instead.
}
const fiber = renderer.findFiberByHostInstance(hostInstance);
if (fiber != null) {
if (!findNearestUnfilteredAncestor) {
// TODO: Remove this option. It's not used.
return getFiberIDThrows(fiber);
}
return findNearestUnfilteredElementID(fiber);
const instance = publicInstanceToDevToolsInstanceMap.get(publicInstance);
if (instance !== undefined) {
return instance.id;
}
return null;
}
@@ -3925,7 +4024,7 @@ export function attach(
owner = ownerFiber._debugOwner;
} else {
const ownerInfo: ReactComponentInfo = (owner: any); // Refined
if (!shouldFilterVirtual(ownerInfo)) {
if (!shouldFilterVirtual(ownerInfo, null)) {
return ownerInfo;
}
owner = ownerInfo.owner;
@@ -5723,7 +5822,7 @@ export function attach(
flushInitialOperations,
getBestMatchForTrackedPath,
getDisplayNameForElementID,
getNearestMountedHostInstance,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getInstanceAndStyle,
getOwnersList,
@@ -5750,5 +5849,6 @@ export function attach(
storeAsGlobal,
unpatchConsoleForStrictMode,
updateComponentFilters,
getEnvironmentNames,
};
}

View File

@@ -73,7 +73,12 @@ export function initBackend(
// Inject any not-yet-injected renderers (if we didn't reload-and-profile)
if (rendererInterface == null) {
if (typeof renderer.findFiberByHostInstance === 'function') {
if (
// v16-19
typeof renderer.findFiberByHostInstance === 'function' ||
// v16.8+
renderer.currentDispatcherRef != null
) {
// react-reconciler v16+
rendererInterface = attach(hook, id, renderer, global);
} else if (renderer.ComponentTree) {

View File

@@ -145,15 +145,13 @@ export function attach(
let getElementIDForHostInstance: GetElementIDForHostInstance =
((null: any): GetElementIDForHostInstance);
let findHostInstanceForInternalID: (id: number) => ?HostInstance;
let getNearestMountedHostInstance = (
node: HostInstance,
): null | HostInstance => {
let getNearestMountedDOMNode = (node: Element): null | Element => {
// Not implemented.
return null;
};
if (renderer.ComponentTree) {
getElementIDForHostInstance = (node, findNearestUnfilteredAncestor) => {
getElementIDForHostInstance = node => {
const internalInstance =
renderer.ComponentTree.getClosestInstanceFromNode(node);
return internalInstanceToIDMap.get(internalInstance) || null;
@@ -162,9 +160,7 @@ export function attach(
const internalInstance = idToInternalInstanceMap.get(id);
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
};
getNearestMountedHostInstance = (
node: HostInstance,
): null | HostInstance => {
getNearestMountedDOMNode = (node: Element): null | Element => {
const internalInstance =
renderer.ComponentTree.getClosestInstanceFromNode(node);
if (internalInstance != null) {
@@ -173,7 +169,7 @@ export function attach(
return null;
};
} else if (renderer.Mount.getID && renderer.Mount.getNode) {
getElementIDForHostInstance = (node, findNearestUnfilteredAncestor) => {
getElementIDForHostInstance = node => {
// Not implemented.
return null;
};
@@ -1078,6 +1074,11 @@ export function attach(
// Not implemented.
}
function getEnvironmentNames(): Array<string> {
// No RSC support.
return [];
}
function setTraceUpdatesEnabled(enabled: boolean) {
// Not implemented.
}
@@ -1121,7 +1122,7 @@ export function attach(
flushInitialOperations,
getBestMatchForTrackedPath,
getDisplayNameForElementID,
getNearestMountedHostInstance,
getNearestMountedDOMNode,
getElementIDForHostInstance,
getInstanceAndStyle,
findHostInstancesForElementID: (id: number) => {
@@ -1152,5 +1153,6 @@ export function attach(
storeAsGlobal,
unpatchConsoleForStrictMode,
updateComponentFilters,
getEnvironmentNames,
};
}

View File

@@ -90,7 +90,6 @@ export type GetDisplayNameForElementID = (id: number) => string | null;
export type GetElementIDForHostInstance = (
component: HostInstance,
findNearestUnfilteredAncestor?: boolean,
) => number | null;
export type FindHostInstancesForElementID = (
id: number,
@@ -106,10 +105,11 @@ export type Lane = number;
export type Lanes = number;
export type ReactRenderer = {
findFiberByHostInstance: (hostInstance: HostInstance) => Fiber | null,
version: string,
rendererPackageName: string,
bundleType: BundleType,
// 16.0+ - To be removed in future versions.
findFiberByHostInstance?: (hostInstance: HostInstance) => Fiber | null,
// 16.9+
overrideHookState?: ?(
fiber: Object,
@@ -358,9 +358,7 @@ export type RendererInterface = {
findHostInstancesForElementID: FindHostInstancesForElementID,
flushInitialOperations: () => void,
getBestMatchForTrackedPath: () => PathMatch | null,
getNearestMountedHostInstance: (
component: HostInstance,
) => HostInstance | null,
getNearestMountedDOMNode: (component: Element) => Element | null,
getElementIDForHostInstance: GetElementIDForHostInstance,
getDisplayNameForElementID: GetDisplayNameForElementID,
getInstanceAndStyle(id: number): InstanceAndStyle,
@@ -416,6 +414,7 @@ export type RendererInterface = {
) => void,
unpatchConsoleForStrictMode: () => void,
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
getEnvironmentNames: () => Array<string>,
// Timeline profiler interface

View File

@@ -189,6 +189,7 @@ export type BackendEvents = {
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],
environmentNames: [Array<string>],
profilingData: [ProfilingDataBackend],
profilingStatus: [boolean],
reloadAppForProfiling: [],
@@ -237,6 +238,7 @@ type FrontendEvents = {
stopProfiling: [],
storeAsGlobal: [StoreAsGlobalParams],
updateComponentFilters: [Array<ComponentFilter>],
getEnvironmentNames: [],
updateConsolePatchSettings: [ConsolePatchSettings],
viewAttributeSource: [ViewAttributeSourceParams],
viewElementSource: [ElementAndRendererID],

View File

@@ -15,6 +15,7 @@ import {
useMemo,
useRef,
useState,
use,
} from 'react';
import {
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
@@ -31,6 +32,7 @@ import {
ComponentFilterElementType,
ComponentFilterHOC,
ComponentFilterLocation,
ComponentFilterEnvironmentName,
ElementTypeClass,
ElementTypeContext,
ElementTypeFunction,
@@ -52,11 +54,16 @@ import type {
ElementType,
ElementTypeComponentFilter,
RegExpComponentFilter,
EnvironmentNameComponentFilter,
} from 'react-devtools-shared/src/frontend/types';
const vscodeFilepath = 'vscode://file/{path}:{line}';
export default function ComponentsSettings(_: {}): React.Node {
export default function ComponentsSettings({
environmentNames,
}: {
environmentNames: Promise<Array<string>>,
}): React.Node {
const store = useContext(StoreContext);
const {parseHookNames, setParseHookNames} = useContext(SettingsContext);
@@ -101,6 +108,30 @@ export default function ComponentsSettings(_: {}): React.Node {
Array<ComponentFilter>,
>(() => [...store.componentFilters]);
const usedEnvironmentNames = use(environmentNames);
const resolvedEnvironmentNames = useMemo(() => {
const set = new Set(usedEnvironmentNames);
// If there are other filters already specified but are not currently
// on the page, we still allow them as options.
for (let i = 0; i < componentFilters.length; i++) {
const filter = componentFilters[i];
if (filter.type === ComponentFilterEnvironmentName) {
set.add(filter.value);
}
}
// Client is special and is always available as a default.
if (set.size > 0) {
// Only show any options at all if there's any other option already
// used by a filter or if any environments are used by the page.
// Note that "Client" can have been added above which would mean
// that we should show it as an option regardless if it's the only
// option.
set.add('Client');
}
return Array.from(set).sort();
}, [usedEnvironmentNames, componentFilters]);
const addFilter = useCallback(() => {
setComponentFilters(prevComponentFilters => {
return [
@@ -146,6 +177,13 @@ export default function ComponentsSettings(_: {}): React.Node {
isEnabled: componentFilter.isEnabled,
isValid: true,
};
} else if (type === ComponentFilterEnvironmentName) {
cloned[index] = {
type: ComponentFilterEnvironmentName,
isEnabled: componentFilter.isEnabled,
isValid: true,
value: 'Client',
};
}
}
return cloned;
@@ -210,6 +248,29 @@ export default function ComponentsSettings(_: {}): React.Node {
[],
);
const updateFilterValueEnvironmentName = useCallback(
(componentFilter: ComponentFilter, value: string) => {
if (componentFilter.type !== ComponentFilterEnvironmentName) {
throw Error('Invalid value for environment name filter');
}
setComponentFilters(prevComponentFilters => {
const cloned: Array<ComponentFilter> = [...prevComponentFilters];
if (componentFilter.type === ComponentFilterEnvironmentName) {
const index = prevComponentFilters.indexOf(componentFilter);
if (index >= 0) {
cloned[index] = {
...componentFilter,
value,
};
}
}
return cloned;
});
},
[],
);
const removeFilter = useCallback((index: number) => {
setComponentFilters(prevComponentFilters => {
const cloned: Array<ComponentFilter> = [...prevComponentFilters];
@@ -246,6 +307,11 @@ export default function ComponentsSettings(_: {}): React.Node {
...((cloned[index]: any): BooleanComponentFilter),
isEnabled,
};
} else if (componentFilter.type === ComponentFilterEnvironmentName) {
cloned[index] = {
...((cloned[index]: any): EnvironmentNameComponentFilter),
isEnabled,
};
}
}
return cloned;
@@ -380,10 +446,16 @@ export default function ComponentsSettings(_: {}): React.Node {
<option value={ComponentFilterDisplayName}>name</option>
<option value={ComponentFilterElementType}>type</option>
<option value={ComponentFilterHOC}>hoc</option>
{resolvedEnvironmentNames.length > 0 && (
<option value={ComponentFilterEnvironmentName}>
environment
</option>
)}
</select>
</td>
<td className={styles.TableCell}>
{componentFilter.type === ComponentFilterElementType &&
{(componentFilter.type === ComponentFilterElementType ||
componentFilter.type === ComponentFilterEnvironmentName) &&
'equals'}
{(componentFilter.type === ComponentFilterLocation ||
componentFilter.type === ComponentFilterDisplayName) &&
@@ -428,6 +500,23 @@ export default function ComponentsSettings(_: {}): React.Node {
value={componentFilter.value}
/>
)}
{componentFilter.type === ComponentFilterEnvironmentName && (
<select
className={styles.Select}
value={componentFilter.value}
onChange={({currentTarget}) =>
updateFilterValueEnvironmentName(
componentFilter,
currentTarget.value,
)
}>
{resolvedEnvironmentNames.map(name => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
)}
</td>
<td className={styles.TableCell}>
<Button

View File

@@ -58,7 +58,8 @@ export default function SettingsModal(_: {}): React.Node {
}
function SettingsModalImpl(_: {}) {
const {setIsModalShowing} = useContext(SettingsModalContext);
const {setIsModalShowing, environmentNames} =
useContext(SettingsModalContext);
const dismissModal = useCallback(
() => setIsModalShowing(false),
[setIsModalShowing],
@@ -81,7 +82,7 @@ function SettingsModalImpl(_: {}) {
let view = null;
switch (selectedTabID) {
case 'components':
view = <ComponentsSettings />;
view = <ComponentsSettings environmentNames={environmentNames} />;
break;
// $FlowFixMe[incompatible-type] is this missing in TabID?
case 'debugging':

View File

@@ -10,7 +10,16 @@
import type {ReactContext} from 'shared/ReactTypes';
import * as React from 'react';
import {createContext, useMemo, useState} from 'react';
import {
createContext,
useContext,
useCallback,
useState,
startTransition,
} from 'react';
import {BridgeContext} from '../context';
import type {FrontendBridge} from '../../../bridge';
export type DisplayDensity = 'comfortable' | 'compact';
export type Theme = 'auto' | 'light' | 'dark';
@@ -18,7 +27,7 @@ export type Theme = 'auto' | 'light' | 'dark';
type Context = {
isModalShowing: boolean,
setIsModalShowing: (value: boolean) => void,
...
environmentNames: null | Promise<Array<string>>,
};
const SettingsModalContext: ReactContext<Context> = createContext<Context>(
@@ -26,20 +35,42 @@ const SettingsModalContext: ReactContext<Context> = createContext<Context>(
);
SettingsModalContext.displayName = 'SettingsModalContext';
function fetchEnvironmentNames(bridge: FrontendBridge): Promise<Array<string>> {
return new Promise(resolve => {
function onEnvironmentNames(names: Array<string>) {
bridge.removeListener('environmentNames', onEnvironmentNames);
resolve(names);
}
bridge.addListener('environmentNames', onEnvironmentNames);
bridge.send('getEnvironmentNames');
});
}
function SettingsModalContextController({
children,
}: {
children: React$Node,
}): React.Node {
const [isModalShowing, setIsModalShowing] = useState<boolean>(false);
const bridge = useContext(BridgeContext);
const value = useMemo(
() => ({isModalShowing, setIsModalShowing}),
[isModalShowing, setIsModalShowing],
);
const setIsModalShowing: boolean => void = useCallback((value: boolean) => {
startTransition(() => {
setContext({
isModalShowing: value,
setIsModalShowing,
environmentNames: value ? fetchEnvironmentNames(bridge) : null,
});
});
});
const [currentContext, setContext] = useState<Context>({
isModalShowing: false,
setIsModalShowing,
environmentNames: null,
});
return (
<SettingsModalContext.Provider value={value}>
<SettingsModalContext.Provider value={currentContext}>
{children}
</SettingsModalContext.Provider>
);

View File

@@ -76,8 +76,9 @@ export const ComponentFilterElementType = 1;
export const ComponentFilterDisplayName = 2;
export const ComponentFilterLocation = 3;
export const ComponentFilterHOC = 4;
export const ComponentFilterEnvironmentName = 5;
export type ComponentFilterType = 1 | 2 | 3 | 4;
export type ComponentFilterType = 1 | 2 | 3 | 4 | 5;
// Hide all elements of types in this Set.
// We hide host components only by default.
@@ -102,10 +103,18 @@ export type BooleanComponentFilter = {
type: 4,
};
export type EnvironmentNameComponentFilter = {
isEnabled: boolean,
isValid: boolean,
type: 5,
value: string,
};
export type ComponentFilter =
| BooleanComponentFilter
| ElementTypeComponentFilter
| RegExpComponentFilter;
| RegExpComponentFilter
| EnvironmentNameComponentFilter;
export type HookName = string | null;
// Map of hook source ("<filename>:<line-number>:<column-number>") to name.

View File

@@ -404,10 +404,16 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
@@ -503,10 +509,16 @@ export function resetWorkInProgress(
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
if (enableProfilerTimer) {
// Note: We don't reset the actualTime counts. It's useful to accumulate

View File

@@ -637,6 +637,18 @@ function finishRenderingHooks<Props, SecondArg>(
): void {
if (__DEV__) {
workInProgress._debugHookTypes = hookTypesDev;
// Stash the thenable state for use by DevTools.
if (workInProgress.dependencies === null) {
if (thenableState !== null) {
workInProgress.dependencies = {
lanes: NoLanes,
firstContext: null,
_debugThenableState: thenableState,
};
}
} else {
workInProgress.dependencies._debugThenableState = thenableState;
}
}
// We can assume the previous dispatcher is always this one, since we set it

View File

@@ -825,10 +825,16 @@ function readContextForConsumer_withSelect<C>(
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}
@@ -869,10 +875,16 @@ function readContextForConsumer<C>(
// This is the first dependency for this component. Create a new list.
lastContextDependency = contextItem;
consumer.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
consumer.dependencies = __DEV__
? {
lanes: NoLanes,
firstContext: contextItem,
_debugThenableState: null,
}
: {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
consumer.flags |= NeedsPropagation;
}

View File

@@ -37,6 +37,7 @@ import type {
} from './ReactFiberTracingMarkerComponent';
import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates';
import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack';
import type {ThenableState} from './ReactFiberThenable';
// Unwind Circular: moved from ReactFiberHooks.old
export type HookType =
@@ -81,7 +82,7 @@ export type Dependencies = {
| ContextDependency<mixed>
| ContextDependencyWithSelect<mixed>
| null,
...
_debugThenableState?: null | ThenableState, // DEV-only
};
export type MemoCache = {