Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 148 additions & 21 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5721,6 +5721,7 @@ export function attach(

function getSuspendedByOfSuspenseNode(
suspenseNode: SuspenseNode,
filterByChildInstance: null | DevToolsInstance, // only include suspended by instances in this subtree
): Array<SerializedAsyncInfo> {
// Collect all ReactAsyncInfo that was suspending this SuspenseNode but
// isn't also in any parent set.
Expand All @@ -5733,6 +5734,15 @@ export function attach(
// to a specific instance will have those appear in order of when that instance was discovered.
let hooksCacheKey: null | DevToolsInstance = null;
let hooksCache: null | HooksTree = null;
// Collect the stream entries with the highest byte offset and end time.
const streamEntries: Map<
Promise<mixed>,
{
asyncInfo: ReactAsyncInfo,
instance: DevToolsInstance,
hooks: null | HooksTree,
},
> = new Map();
suspenseNode.suspendedBy.forEach((set, ioInfo) => {
let parentNode = suspenseNode.parent;
while (parentNode !== null) {
Expand All @@ -5747,8 +5757,30 @@ export function attach(
if (set.size === 0) {
return;
}
const firstInstance: DevToolsInstance = (set.values().next().value: any);
if (firstInstance.suspendedBy !== null) {
let firstInstance: null | DevToolsInstance = null;
if (filterByChildInstance === null) {
firstInstance = (set.values().next().value: any);
} else {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const childInstance of set.values()) {
if (firstInstance === null) {
firstInstance = childInstance;
}
if (
childInstance !== filterByChildInstance &&
!isChildOf(
filterByChildInstance,
childInstance,
suspenseNode.instance,
)
) {
// Something suspended on this outside the filtered instance. That means that
// it is not unique to just this filtered instance so we skip including it.
return;
}
}
}
if (firstInstance !== null && firstInstance.suspendedBy !== null) {
const asyncInfo = getAwaitInSuspendedByFromIO(
firstInstance.suspendedBy,
ioInfo,
Expand All @@ -5771,13 +5803,113 @@ export function attach(
}
}
}
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
const newIO = asyncInfo.awaited;
if (newIO.name === 'RSC stream' && newIO.value != null) {
const streamPromise = newIO.value;
// Special case RSC stream entries to pick the last entry keyed by the stream.
const existingEntry = streamEntries.get(streamPromise);
if (existingEntry === undefined) {
streamEntries.set(streamPromise, {
asyncInfo,
instance: firstInstance,
hooks,
});
} else {
const existingIO = existingEntry.asyncInfo.awaited;
if (
newIO !== existingIO &&
((newIO.byteSize !== undefined &&
existingIO.byteSize !== undefined &&
newIO.byteSize > existingIO.byteSize) ||
newIO.end > existingIO.end)
) {
// The new entry is later in the stream that the old entry. Replace it.
existingEntry.asyncInfo = asyncInfo;
existingEntry.instance = firstInstance;
existingEntry.hooks = hooks;
}
}
} else {
result.push(serializeAsyncInfo(asyncInfo, firstInstance, hooks));
}
}
}
});
// Add any deduped stream entries.
streamEntries.forEach(({asyncInfo, instance, hooks}) => {
result.push(serializeAsyncInfo(asyncInfo, instance, hooks));
});
return result;
}

function getSuspendedByOfInstance(
devtoolsInstance: DevToolsInstance,
hooks: null | HooksTree,
): Array<SerializedAsyncInfo> {
const suspendedBy = devtoolsInstance.suspendedBy;
if (suspendedBy === null) {
return [];
}

const foundIOEntries: Set<ReactIOInfo> = new Set();
const streamEntries: Map<Promise<mixed>, ReactAsyncInfo> = new Map();
const result: Array<SerializedAsyncInfo> = [];
for (let i = 0; i < suspendedBy.length; i++) {
const asyncInfo = suspendedBy[i];
const ioInfo = asyncInfo.awaited;
if (foundIOEntries.has(ioInfo)) {
// We have already added this I/O entry to the result. We can dedupe it.
// This can happen when an instance depends on the same data in mutliple places.
continue;
}
foundIOEntries.add(ioInfo);
if (ioInfo.name === 'RSC stream' && ioInfo.value != null) {
const streamPromise = ioInfo.value;
// Special case RSC stream entries to pick the last entry keyed by the stream.
const existingEntry = streamEntries.get(streamPromise);
if (existingEntry === undefined) {
streamEntries.set(streamPromise, asyncInfo);
} else {
const existingIO = existingEntry.awaited;
if (
ioInfo !== existingIO &&
((ioInfo.byteSize !== undefined &&
existingIO.byteSize !== undefined &&
ioInfo.byteSize > existingIO.byteSize) ||
ioInfo.end > existingIO.end)
) {
// The new entry is later in the stream that the old entry. Replace it.
streamEntries.set(streamPromise, asyncInfo);
}
}
} else {
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
}
}
// Add any deduped stream entries.
streamEntries.forEach(asyncInfo => {
result.push(serializeAsyncInfo(asyncInfo, devtoolsInstance, hooks));
});
return result;
}

function getSuspendedByOfInstanceSubtree(
devtoolsInstance: DevToolsInstance,
): Array<SerializedAsyncInfo> {
// Get everything suspending below this instance down to the next Suspense node.
// First find the parent Suspense boundary which will have accumulated everything
let suspenseParentInstance = devtoolsInstance;
while (suspenseParentInstance.suspenseNode === null) {
if (suspenseParentInstance.parent === null) {
// We don't expect to hit this. We should always find the root.
return [];
}
suspenseParentInstance = suspenseParentInstance.parent;
}
const suspenseNode: SuspenseNode = suspenseParentInstance.suspenseNode;
return getSuspendedByOfSuspenseNode(suspenseNode, devtoolsInstance);
}

const FALLBACK_THROTTLE_MS: number = 300;

function getSuspendedByRange(
Expand Down Expand Up @@ -6291,17 +6423,17 @@ export function attach(
fiberInstance.suspenseNode !== null
? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
// this boundary down to the next Suspense boundary.
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode)
: // This set is an edge case where if you pass a promise to a Client Component into a children
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
// In this case, this becomes associated with the Client/Host Component where as normally
// you'd expect these to be associated with the Server Component that awaited the data.
// TODO: Prepend other suspense sources like css, images and use().
fiberInstance.suspendedBy === null
? []
: fiberInstance.suspendedBy.map(info =>
serializeAsyncInfo(info, fiberInstance, hooks),
);
getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode, null)
: tag === ActivityComponent
? // For Activity components we show everything that suspends the subtree down to the next boundary
// so that you can see what suspends a Transition at that level.
getSuspendedByOfInstanceSubtree(fiberInstance)
: // This set is an edge case where if you pass a promise to a Client Component into a children
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
// In this case, this becomes associated with the Client/Host Component where as normally
// you'd expect these to be associated with the Server Component that awaited the data.
// TODO: Prepend other suspense sources like css, images and use().
getSuspendedByOfInstance(fiberInstance, hooks);
const suspendedByRange = getSuspendedByRange(
getNearestSuspenseNode(fiberInstance),
);
Expand Down Expand Up @@ -6446,7 +6578,7 @@ export function attach(

const isSuspended = null;
// Things that Suspended this Server Component (use(), awaits and direct child promises)
const suspendedBy = virtualInstance.suspendedBy;
const suspendedBy = getSuspendedByOfInstance(virtualInstance, null);
const suspendedByRange = getSuspendedByRange(
getNearestSuspenseNode(virtualInstance),
);
Expand Down Expand Up @@ -6497,12 +6629,7 @@ export function attach(
? []
: Array.from(componentLogsEntry.warnings.entries()),

suspendedBy:
suspendedBy === null
? []
: suspendedBy.map(info =>
serializeAsyncInfo(info, virtualInstance, null),
),
suspendedBy: suspendedBy,
suspendedByRange: suspendedByRange,
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@
*/

import * as React from 'react';
import {useContext} from 'react';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {useState, useContext, useCallback} from 'react';

import SearchInput from '../SearchInput';
import SearchInput from 'react-devtools-shared/src/devtools/views/SearchInput';
import {
TreeDispatcherContext,
TreeStateContext,
} from 'react-devtools-shared/src/devtools/views/Components/TreeContext';

type Props = {};
export default function ComponentSearchInput(): React.Node {
const [localSearchQuery, setLocalSearchQuery] = useState('');
const {searchIndex, searchResults} = useContext(TreeStateContext);
const transitionDispatch = useContext(TreeDispatcherContext);

export default function ComponentSearchInput(props: Props): React.Node {
const {searchIndex, searchResults, searchText} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);

const search = (text: string) =>
dispatch({type: 'SET_SEARCH_TEXT', payload: text});
const goToNextResult = () => dispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'});
const goToPreviousResult = () =>
dispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'});
const search = useCallback(
(text: string) => {
setLocalSearchQuery(text);
transitionDispatch({type: 'SET_SEARCH_TEXT', payload: text});
},
[setLocalSearchQuery, transitionDispatch],
);
const goToNextResult = useCallback(
() => transitionDispatch({type: 'GO_TO_NEXT_SEARCH_RESULT'}),
[transitionDispatch],
);
const goToPreviousResult = useCallback(
() => transitionDispatch({type: 'GO_TO_PREVIOUS_SEARCH_RESULT'}),
[transitionDispatch],
);

return (
<SearchInput
Expand All @@ -33,7 +45,7 @@ export default function ComponentSearchInput(props: Props): React.Node {
search={search}
searchIndex={searchIndex}
searchResultsCount={searchResults.length}
searchText={searchText}
searchText={localSearchQuery}
testName="ComponentSearchInput"
/>
);
Expand Down
21 changes: 7 additions & 14 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -2046,24 +2046,17 @@ function customizeViewTransitionError(
error.message ===
'Skipping view transition because document visibility state has become hidden.' ||
error.message ===
'Skipping view transition because viewport size changed.'
'Skipping view transition because viewport size changed.' ||
// Chrome uses a generic error message instead of specific reasons. It will log a
// more specific reason in the console but the user might not look there.
// Some of these errors are important to surface like duplicate name errors but
// it's too noisy for unactionable cases like the document was hidden. Therefore,
// we hide all of them and hopefully it surfaces in another browser.
error.message === 'Transition was aborted because of invalid state'
) {
// Skip logging this. This is not considered an error.
return null;
}
if (__DEV__) {
if (
error.message === 'Transition was aborted because of invalid state'
) {
// Chrome doesn't include the reason in the message but logs it in the console..
// Redirect the user to look there.
// eslint-disable-next-line react-internal/prod-error-codes
return new Error(
'A ViewTransition could not start. See the console for more details.',
{cause: error},
);
}
}
break;
}
}
Expand Down
Loading