diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 1c4443e5a49a0..55974db14ce17 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -393,7 +393,7 @@ function* generateInstructionTypes( shapeId: BuiltInArrayId, }); } else { - break; + continue; } } } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md new file mode 100644 index 0000000000000..423076cc3a4b1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +function Component(props) { + // Intentionally don't bind state, this repros a bug where we didn't + // infer the type of destructured properties after a hole in the array + let [, setState] = useState(); + setState(1); + return props.foo; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ['TodoAdd'], + isComponent: 'TodoAdd', +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Calling setState during render may trigger an infinite loop + +Calling setState during render will trigger another render, and can lead to infinite loops. (https://react.dev/reference/react/useState). + +error.invalid-setState-in-render-unbound-state.ts:5:2 + 3 | // infer the type of destructured properties after a hole in the array + 4 | let [, setState] = useState(); +> 5 | setState(1); + | ^^^^^^^^ Found setState() in render + 6 | return props.foo; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js new file mode 100644 index 0000000000000..58e2837692a1d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-setState-in-render-unbound-state.js @@ -0,0 +1,13 @@ +function Component(props) { + // Intentionally don't bind state, this repros a bug where we didn't + // infer the type of destructured properties after a hole in the array + let [, setState] = useState(); + setState(1); + return props.foo; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: ['TodoAdd'], + isComponent: 'TodoAdd', +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.expect.md deleted file mode 100644 index 160699b115ba9..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.expect.md +++ /dev/null @@ -1,35 +0,0 @@ - -## Input - -```javascript -function t(props) { - let [, setstate] = useState(); - setstate(1); - return props.foo; -} - -export const FIXTURE_ENTRYPOINT = { - fn: t, - params: ['TodoAdd'], - isComponent: 'TodoAdd', -}; - -``` - -## Code - -```javascript -function t(props) { - const [, setstate] = useState(); - setstate(1); - return props.foo; -} - -export const FIXTURE_ENTRYPOINT = { - fn: t, - params: ["TodoAdd"], - isComponent: "TodoAdd", -}; - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.js deleted file mode 100644 index d76b88f579190..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/holey-array.js +++ /dev/null @@ -1,11 +0,0 @@ -function t(props) { - let [, setstate] = useState(); - setstate(1); - return props.foo; -} - -export const FIXTURE_ENTRYPOINT = { - fn: t, - params: ['TodoAdd'], - isComponent: 'TodoAdd', -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.expect.md new file mode 100644 index 0000000000000..c5bfe197c3afc --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useCallback, useTransition} from 'react'; + +function useFoo() { + const [, /* isPending intentionally not captured */ start] = useTransition(); + + return useCallback(() => { + start(); + }, []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useCallback, useTransition } from "react"; + +function useFoo() { + const $ = _c(1); + const [, start] = useTransition(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + start(); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.js new file mode 100644 index 0000000000000..d7560197f0311 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-transition-no-ispending.js @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useCallback, useTransition} from 'react'; + +function useFoo() { + const [, /* isPending intentionally not captured */ start] = useTransition(); + + return useCallback(() => { + start(); + }, []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.expect.md new file mode 100644 index 0000000000000..8444126755849 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useCallback, useTransition} from 'react'; + +function useFoo() { + const [, /* state value intentionally not captured */ setState] = useState(); + + return useCallback(() => { + setState(x => x + 1); + }, []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees +import { useCallback, useTransition } from "react"; + +function useFoo() { + const $ = _c(1); + const [, setState] = useState(); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + setState(_temp); + }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} +function _temp(x) { + return x + 1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: exception) useState is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.js new file mode 100644 index 0000000000000..e270d4f0192f9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-use-memo-unused-state.js @@ -0,0 +1,15 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useCallback, useTransition} from 'react'; + +function useFoo() { + const [, /* state value intentionally not captured */ setState] = useState(); + + return useCallback(() => { + setState(x => x + 1); + }, []); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 688f1b473bf62..b7fe41b96c5b4 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2139,8 +2139,8 @@ export function attach( // Regular operations pendingOperations.length + // All suspender changes are batched in a single message. - // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]] - (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0), + // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]] + (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0), ); // Identify which renderer this update is coming from. @@ -2225,6 +2225,14 @@ export function attach( } operations[i++] = fiberIdWithChanges; operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0; + const instance = suspense.instance; + const isSuspended = + // TODO: Track if other SuspenseNode like SuspenseList rows are suspended. + (instance.kind === FIBER_INSTANCE || + instance.kind === FILTERED_FIBER_INSTANCE) && + instance.data.tag === SuspenseComponent && + instance.data.memoizedState !== null; + operations[i++] = isSuspended ? 1 : 0; operations[i++] = suspense.environments.size; suspense.environments.forEach((count, env) => { operations[i++] = getStringID(env); @@ -2251,7 +2259,10 @@ export function attach( if (typeof instance !== 'object' || instance === null) { return null; } - if (typeof instance.getClientRects === 'function') { + if ( + typeof instance.getClientRects === 'function' || + instance.nodeType === 3 + ) { // DOM const doc = instance.ownerDocument; if (instance === doc.documentElement) { @@ -2273,7 +2284,21 @@ export function attach( const win = doc && doc.defaultView; const scrollX = win ? win.scrollX : 0; const scrollY = win ? win.scrollY : 0; - const rects = instance.getClientRects(); + let rects; + if (instance.nodeType === 3) { + // Text nodes cannot be measured directly but we can measure a Range. + if (typeof doc.createRange !== 'function') { + return null; + } + const range = doc.createRange(); + if (typeof range.getClientRects !== 'function') { + return null; + } + range.selectNodeContents(instance); + rects = range.getClientRects(); + } else { + rects = instance.getClientRects(); + } for (let i = 0; i < rects.length; i++) { const rect = rects[i]; result.push({ @@ -2640,9 +2665,15 @@ export function attach( const fiber = fiberInstance.data; const props = fiber.memoizedProps; // TODO: Compute a fallback name based on Owner, key etc. - const name = props === null ? null : props.name || null; + const name = + fiber.tag !== SuspenseComponent || props === null + ? null + : props.name || null; const nameStringID = getStringID(name); + const isSuspended = + fiber.tag === SuspenseComponent && fiber.memoizedState !== null; + if (__DEBUG__) { console.log('recordSuspenseMount()', suspenseInstance); } @@ -2653,6 +2684,7 @@ export function attach( pushOperation(fiberID); pushOperation(parentID); pushOperation(nameStringID); + pushOperation(isSuspended ? 1 : 0); const rects = suspenseInstance.rects; if (rects === null) { @@ -3262,14 +3294,22 @@ export function attach( // We don't update rects inside disconnected subtrees. return; } - const nextRects = measureInstance(suspenseNode.instance); - const prevRects = suspenseNode.rects; - if (areEqualRects(prevRects, nextRects)) { - return; // Unchanged + const instance = suspenseNode.instance; + + const isSuspendedSuspenseComponent = + (instance.kind === FIBER_INSTANCE || + instance.kind === FILTERED_FIBER_INSTANCE) && + instance.data.tag === SuspenseComponent && + instance.data.memoizedState !== null; + if (isSuspendedSuspenseComponent) { + // This boundary itself was suspended and we don't measure those since that would measure + // the fallback. We want to keep a ghost of the rectangle of the content not currently shown. + return; } - // The rect has changed. While the bailed out root wasn't in a disconnected subtree, + + // While this boundary wasn't suspended and the bailed out root and wasn't in a disconnected subtree, // it's possible that this node was in one. So we need to check if we're offscreen. - let parent = suspenseNode.instance.parent; + let parent = instance.parent; while (parent !== null) { if ( (parent.kind === FIBER_INSTANCE || @@ -3285,6 +3325,13 @@ export function attach( } parent = parent.parent; } + + const nextRects = measureInstance(suspenseNode.instance); + const prevRects = suspenseNode.rects; + if (areEqualRects(prevRects, nextRects)) { + return; // Unchanged + } + // We changed inside a visible tree. // Since this boundary changed, it's possible it also affected its children so lets // measure them as well. @@ -5006,15 +5053,24 @@ export function attach( const nextIsSuspended = isSuspendedOffscreen(nextFiber); if (isLegacySuspense) { - if ( - fiberInstance !== null && - fiberInstance.suspenseNode !== null && - (prevFiber.stateNode === null) !== (nextFiber.stateNode === null) - ) { - trackThrownPromisesFromRetryCache( - fiberInstance.suspenseNode, - nextFiber.stateNode, - ); + if (fiberInstance !== null && fiberInstance.suspenseNode !== null) { + const suspenseNode = fiberInstance.suspenseNode; + if ( + (prevFiber.stateNode === null) !== + (nextFiber.stateNode === null) + ) { + trackThrownPromisesFromRetryCache( + suspenseNode, + nextFiber.stateNode, + ); + } + if ( + (prevFiber.memoizedState === null) !== + (nextFiber.memoizedState === null) + ) { + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); + } } } // The logic below is inspired by the code paths in updateSuspenseComponent() @@ -5162,6 +5218,14 @@ export function attach( ); } + if ( + (prevFiber.memoizedState === null) !== + (nextFiber.memoizedState === null) + ) { + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); + } + shouldMeasureSuspenseNode = false; updateFlags |= updateSuspenseChildrenRecursively( nextContentFiber, @@ -5188,6 +5252,8 @@ export function attach( } trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode); + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); mountSuspenseChildrenRecursively( nextContentFiber, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 46709c9a8048c..1262a8d4647a6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -417,6 +417,7 @@ export function attach( pushOperation(id); pushOperation(parentID); pushOperation(getStringID(null)); // name + pushOperation(0); // isSuspended // TODO: Measure rect of root pushOperation(-1); } else { diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js index fdd059cc23486..e55bdb4298c47 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/Overlay.js @@ -187,10 +187,13 @@ export default class Overlay { } } - inspect(nodes: $ReadOnlyArray, name?: ?string) { + inspect(nodes: $ReadOnlyArray, name?: ?string) { // We can't get the size of text nodes or comment nodes. React as of v15 // heavily uses comment nodes to delimit text. - const elements = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE); + // TODO: We actually can measure text nodes. We should. + const elements: $ReadOnlyArray = (nodes.filter( + node => node.nodeType === Node.ELEMENT_NODE, + ): any); while (this.rects.length > elements.length) { const rect = this.rects.pop(); diff --git a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js index 6fd93d3519c87..894c4fba94404 100644 --- a/packages/react-devtools-shared/src/backend/views/Highlighter/index.js +++ b/packages/react-devtools-shared/src/backend/views/Highlighter/index.js @@ -366,8 +366,6 @@ export default function setupHighlighter( // Don't pass the name explicitly. // It will be inferred from DOM tag and Fiber owner. showOverlay([target], null, agent, false); - - selectElementForNode(target); } function onPointerUp(event: MouseEvent) { diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index b6229192c23b1..3162dc215ff0a 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -217,10 +217,15 @@ export type BackendEvents = { selectElement: [number], shutdown: [], stopInspectingHost: [boolean], - syncSelectionFromBuiltinElementsPanel: [], syncSelectionToBuiltinElementsPanel: [], unsupportedRendererVersion: [], + extensionComponentsPanelShown: [], + extensionComponentsPanelHidden: [], + + resumeElementPolling: [], + pauseElementPolling: [], + // React Native style editor plug-in. isNativeStyleEditorSupported: [ {isSupported: boolean, validAttributes: ?$ReadOnlyArray}, @@ -240,8 +245,6 @@ type FrontendEvents = { clearWarningsForElementID: [ElementAndRendererID], copyElementPath: [CopyElementPathParams], deletePath: [DeletePath], - extensionComponentsPanelShown: [], - extensionComponentsPanelHidden: [], getBackendVersion: [], getBridgeProtocol: [], getIfHasUnsupportedRendererVersion: [], @@ -265,7 +268,7 @@ type FrontendEvents = { shutdown: [], startInspectingHost: [], startProfiling: [StartProfilingParams], - stopInspectingHost: [boolean], + stopInspectingHost: [], scrollToHostInstance: [ScrollToHostInstance], stopProfiling: [], storeAsGlobal: [StoreAsGlobalParams], @@ -275,6 +278,8 @@ type FrontendEvents = { viewAttributeSource: [ViewAttributeSourceParams], viewElementSource: [ElementAndRendererID], + syncSelectionFromBuiltinElementsPanel: [], + // React Native style editor plug-in. NativeStyleEditor_measure: [ElementAndRendererID], NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams], @@ -295,19 +300,13 @@ type FrontendEvents = { overrideProps: [OverrideValue], overrideState: [OverrideValue], - resumeElementPolling: [], - pauseElementPolling: [], - getHookSettings: [], }; class Bridge< OutgoingEvents: Object, IncomingEvents: Object, -> extends EventEmitter<{ - ...IncomingEvents, - ...OutgoingEvents, -}> { +> extends EventEmitter { _isShutdown: boolean = false; _messageQueue: Array = []; _scheduledFlush: boolean = false; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index eeb6da60f8aeb..86961f5bd91fb 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -1552,7 +1552,8 @@ export default class Store extends EventEmitter<{ const id = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = ((operations[i + 4]: any): number); + const isSuspended = operations[i + 4] === 1; + const numRects = ((operations[i + 5]: any): number); let name = stringTable[nameStringID]; if (this._idToSuspense.has(id)) { @@ -1579,7 +1580,7 @@ export default class Store extends EventEmitter<{ } } - i += 5; + i += 6; let rects: SuspenseNode['rects']; if (numRects === -1) { rects = null; @@ -1625,6 +1626,7 @@ export default class Store extends EventEmitter<{ name, rects, hasUniqueSuspenders: false, + isSuspended: isSuspended, }); hasSuspenseTreeChanged = true; @@ -1801,6 +1803,7 @@ export default class Store extends EventEmitter<{ for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const id = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; const environmentNames = []; for ( @@ -1832,6 +1835,7 @@ export default class Store extends EventEmitter<{ } suspense.hasUniqueSuspenders = hasUniqueSuspenders; + suspense.isSuspended = isSuspended; // TODO: Recompute the environment names. } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js index 2f2b9e9e91d0a..17a7b049cc9b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectHostNodesToggle.js @@ -26,7 +26,7 @@ export default function InspectHostNodesToggle(): React.Node { logEvent({event_name: 'inspect-element-button-clicked'}); bridge.send('startInspectingHost'); } else { - bridge.send('stopInspectingHost', false); + bridge.send('stopInspectingHost'); } }, [bridge], diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 28be5f9e1c7e1..4addf10916693 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -378,7 +378,8 @@ function updateTree( const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = operations[i + 4]; + const isSuspended = operations[i + 4]; + const numRects = operations[i + 5]; const name = stringTable[nameStringID]; if (__DEBUG__) { @@ -388,16 +389,16 @@ function updateTree( } else { rects = '[' + - operations.slice(i + 5, i + 5 + numRects * 4).join(',') + + operations.slice(i + 6, i + 6 + numRects * 4).join(',') + ']'; } debug( 'Add suspense', - `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`, + `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID} suspended ${isSuspended}`, ); } - i += 5 + (numRects === -1 ? 0 : numRects * 4); + i += 6 + (numRects === -1 ? 0 : numRects * 4); break; } @@ -459,12 +460,13 @@ function updateTree( for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const suspenseNodeId = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; i += environmentNamesLength; if (__DEBUG__) { debug( 'Suspender changes', - `Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`, + `Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`, ); } } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css index ba862051d9513..591d908735efb 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -19,11 +19,6 @@ .SuspenseRectsBoundaryChildren { pointer-events: none; - /** - * So that the shadow of Boundaries within is clipped off. - * Otherwise it would look like this boundary is further elevated. - */ - overflow: hidden; } .SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren { @@ -49,6 +44,10 @@ outline-width: 0; } +.SuspenseRectsScaledRect[data-suspended='true'] { + opacity: 0.3; +} + /* highlight this boundary */ .SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect { background-color: var(--color-background-hover); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index f949631be8427..5f07bb61001ee 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -35,11 +35,13 @@ function ScaledRect({ className, rect, visible, + suspended, ...props }: { className: string, rect: Rect, visible: boolean, + suspended: boolean, ... }): React$Node { const viewBox = useContext(ViewBox); @@ -53,6 +55,7 @@ function ScaledRect({ {...props} className={styles.SuspenseRectsScaledRect + ' ' + className} data-visible={visible} + data-suspended={suspended} style={{ width, height, @@ -145,7 +148,8 @@ function SuspenseRects({ + visible={visible} + suspended={suspense.isSuspended}> {visible && suspense.rects !== null && diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js index 53d20b6467c46..f1998aff4b16c 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js @@ -31,9 +31,9 @@ export default function SuspenseScrubber({ max: number, value: number, highlight: number, - onBlur: () => void, + onBlur?: () => void, onChange: (index: number) => void, - onFocus: () => void, + onFocus?: () => void, onHoverSegment: (index: number) => void, onHoverLeave: () => void, }): React$Node { diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js index f7b63fa2c62c2..9ade19c33075b 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.js @@ -85,7 +85,9 @@ function ToggleUniqueSuspenders() { + title={ + 'Filter Suspense which does not suspend, or if the parent also suspend on the same.' + }> ); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js index 8ebb06899d62a..9b70812134288 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -11,7 +11,7 @@ import * as React from 'react'; import {useContext, useEffect} from 'react'; import {BridgeContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; -import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks'; +import {useScrollToHostInstance} from '../hooks'; import { SuspenseTreeDispatcherContext, SuspenseTreeStateContext, @@ -25,8 +25,6 @@ function SuspenseTimelineInput() { const bridge = useContext(BridgeContext); const treeDispatch = useContext(TreeDispatcherContext); const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext); - const {highlightHostInstance, clearHighlightHostInstance} = - useHighlightHostInstance(); const scrollToHostInstance = useScrollToHostInstance(); const {timeline, timelineIndex, hoveredTimelineIndex, playing, autoScroll} = @@ -37,7 +35,6 @@ function SuspenseTimelineInput() { function switchSuspenseNode(nextTimelineIndex: number) { const nextSelectedSuspenseID = timeline[nextTimelineIndex]; - highlightHostInstance(nextSelectedSuspenseID); treeDispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: nextSelectedSuspenseID, @@ -52,23 +49,14 @@ function SuspenseTimelineInput() { switchSuspenseNode(pendingTimelineIndex); } - function handleBlur() { - clearHighlightHostInstance(); - } - function handleFocus() { switchSuspenseNode(timelineIndex); } function handleHoverSegment(hoveredValue: number) { - const suspenseID = timeline[hoveredValue]; - if (suspenseID === undefined) { - throw new Error( - `Suspense node not found for value ${hoveredValue} in timeline.`, - ); - } - highlightHostInstance(suspenseID); + // TODO: Consider highlighting the rect instead. } + function handleUnhoverSegment() {} function skipPrevious() { const nextSelectedSuspenseID = timeline[timelineIndex - 1]; @@ -180,11 +168,10 @@ function SuspenseTimelineInput() { max={max} value={timelineIndex} highlight={hoveredTimelineIndex} - onBlur={handleBlur} onChange={handleChange} onFocus={handleFocus} onHoverSegment={handleHoverSegment} - onHoverLeave={clearHighlightHostInstance} + onHoverLeave={handleUnhoverSegment} /> diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 7762af43e0040..2a012ce33a17d 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -200,6 +200,7 @@ export type SuspenseNode = { name: string | null, rects: null | Array, hasUniqueSuspenders: boolean, + isSuspended: boolean, }; // Serialized version of ReactIOInfo diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 007db77f2202a..29ff6d566bd6f 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -340,9 +340,10 @@ export function printOperationsArray(operations: Array) { const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = operations[i + 4]; + const isSuspended = operations[i + 4]; + const numRects = operations[i + 5]; - i += 5; + i += 6; const name = stringTable[nameStringID]; let rects: string; @@ -368,7 +369,7 @@ export function printOperationsArray(operations: Array) { } logs.push( - `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`, + `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID} suspended ${isSuspended}`, ); break; } @@ -431,10 +432,11 @@ export function printOperationsArray(operations: Array) { for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const id = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; i += environmentNamesLength; logs.push( - `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`, + `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`, ); }