From 3fe51c9e147fb5215011cf3fd9544c5a776abf41 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 29 Aug 2025 12:04:27 +0200 Subject: [PATCH 01/16] [Flight] Use more robust web socket implementation in fixture (#34338) The `WebSocketStream` implementation seems to be a bit unreliable. We've seen `Cannot close a ERRORED writable stream` errors when expanding the logged deep object, for example. And when reducing the fixture to a minimal app, we even get `Connection closed` errors, because the web socket connection is closed before all debug chunks are sent. We can improve the reliability of the web socket connection by using a normal `WebSocket` instance on the client, along with manually creating a `WritableStream` and a `ReadableStream` for processing the messages. As an additional benefit, the debug channel now also works in Firefox and Safari. On the server, we're simplifying the integration with the Express server a bit by utilizing the `server` property for `WebSocket.Server`, instead of the `noServer` property with the manual upgrade handling. --- fixtures/flight/server/region.js | 41 +++++++++------------- fixtures/flight/src/index.js | 58 ++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 39 deletions(-) diff --git a/fixtures/flight/server/region.js b/fixtures/flight/server/region.js index 0f96f3aa74..7339e3a48a 100644 --- a/fixtures/flight/server/region.js +++ b/fixtures/flight/server/region.js @@ -74,13 +74,7 @@ function getDebugChannel(req) { return activeDebugChannels.get(requestId); } -async function renderApp( - res, - returnValue, - formState, - noCache, - promiseForDebugChannel -) { +async function renderApp(res, returnValue, formState, noCache, debugChannel) { const {renderToPipeableStream} = await import( 'react-server-dom-webpack/server' ); @@ -132,7 +126,7 @@ async function renderApp( // For client-invoked server actions we refresh the tree and return a return value. const payload = {root, returnValue, formState}; const {pipe} = renderToPipeableStream(payload, moduleMap, { - debugChannel: await promiseForDebugChannel, + debugChannel, filterStackFrame, }); pipe(res); @@ -385,23 +379,20 @@ app.on('error', function (error) { if (process.env.NODE_ENV === 'development') { // Open a websocket server for Debug information const WebSocket = require('ws'); - const webSocketServer = new WebSocket.Server({noServer: true}); - httpServer.on('upgrade', (request, socket, head) => { - const DEBUG_CHANNEL_PATH = '/debug-channel?'; - if (request.url.startsWith(DEBUG_CHANNEL_PATH)) { - const requestId = request.url.slice(DEBUG_CHANNEL_PATH.length); - const promiseForWs = new Promise(resolve => { - webSocketServer.handleUpgrade(request, socket, head, ws => { - ws.on('close', () => { - activeDebugChannels.delete(requestId); - }); - resolve(ws); - }); - }); - activeDebugChannels.set(requestId, promiseForWs); - } else { - socket.destroy(); - } + const webSocketServer = new WebSocket.Server({ + server: httpServer, + path: '/debug-channel', + }); + + webSocketServer.on('connection', (ws, req) => { + const url = new URL(req.url, `http://${req.headers.host}`); + const requestId = url.searchParams.get('id'); + + activeDebugChannels.set(requestId, ws); + + ws.on('close', (code, reason) => { + activeDebugChannels.delete(requestId); + }); }); } diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js index 7e4e8c801e..447b1957c8 100644 --- a/fixtures/flight/src/index.js +++ b/fixtures/flight/src/index.js @@ -14,18 +14,52 @@ function findSourceMapURL(fileName) { ); } +async function createWebSocketStream(url) { + const ws = new WebSocket(url); + ws.binaryType = 'arraybuffer'; + + await new Promise((resolve, reject) => { + ws.addEventListener('open', resolve, {once: true}); + ws.addEventListener('error', reject, {once: true}); + }); + + const writable = new WritableStream({ + write(chunk) { + ws.send(chunk); + }, + close() { + ws.close(); + }, + abort(reason) { + ws.close(1000, reason && String(reason)); + }, + }); + + const readable = new ReadableStream({ + start(controller) { + ws.addEventListener('message', event => { + controller.enqueue(event.data); + }); + ws.addEventListener('close', () => { + controller.close(); + }); + ws.addEventListener('error', err => { + controller.error(err); + }); + }, + }); + + return {readable, writable}; +} + let updateRoot; async function callServer(id, args) { let response; - if ( - process.env.NODE_ENV === 'development' && - typeof WebSocketStream === 'function' - ) { + if (process.env.NODE_ENV === 'development') { const requestId = crypto.randomUUID(); - const wss = new WebSocketStream( - 'ws://localhost:3001/debug-channel?' + requestId + const debugChannel = await createWebSocketStream( + `ws://localhost:3001/debug-channel?id=${requestId}` ); - const debugChannel = await wss.opened; response = createFromFetch( fetch('/', { method: 'POST', @@ -74,15 +108,11 @@ function Shell({data}) { async function hydrateApp() { let response; - if ( - process.env.NODE_ENV === 'development' && - typeof WebSocketStream === 'function' - ) { + if (process.env.NODE_ENV === 'development') { const requestId = crypto.randomUUID(); - const wss = new WebSocketStream( - 'ws://localhost:3001/debug-channel?' + requestId + const debugChannel = await createWebSocketStream( + `ws://localhost:3001/debug-channel?id=${requestId}` ); - const debugChannel = await wss.opened; response = createFromFetch( fetch('/', { headers: { From aad7c664ffbde52e5d8004b542d83d6d4b7a32a0 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 29 Aug 2025 17:22:39 +0200 Subject: [PATCH 02/16] [Flight] Don't try to close debug channel twice (#34340) When the debug channel was already closed, we must not try to close it again when the Response gets garbage collected. **Test plan:** 1. reduce the Flight fixture `App` component to a minimum [^1] - remove everything from `` - delete the `console.log` statement 2. open the app in Firefox (seems to have a more aggressive GC strategy) 3. wait a few seconds On `main`, you will see the following error in the browser console: ``` TypeError: Can not close stream after closing or error ``` With this change, the error is gone. [^1]: It's a bit concerning that step 1 is needed to reproduce the issue. Either GC is behaving differently with the unmodified App, or we may hold on to the Response under certain conditions, potentially creating a memory leak. This needs further investigation. --- packages/react-client/src/ReactFlightClient.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 61a67bce9d..b966645623 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1010,10 +1010,15 @@ export function reportGlobalError( if (__DEV__) { const debugChannel = response._debugChannel; if (debugChannel !== undefined) { - // If we don't have any more ways of reading data, we don't have to send any - // more neither. So we close the writable side. + // If we don't have any more ways of reading data, we don't have to send + // any more neither. So we close the writable side. closeDebugChannel(debugChannel); response._debugChannel = undefined; + // Make sure the debug channel is not closed a second time when the + // Response gets GC:ed. + if (debugChannelRegistry !== null) { + debugChannelRegistry.unregister(response); + } } } } @@ -2434,7 +2439,7 @@ function ResponseInstance( // When a Response gets GC:ed because nobody is referring to any of the // objects that lazily load from the Response anymore, then we can close // the debug channel. - debugChannelRegistry.register(this, debugChannel); + debugChannelRegistry.register(this, debugChannel, this); } } } From bb6f0c8d2f29754347db0ff28186dc89c128b6ca Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 1 Sep 2025 11:03:57 +0200 Subject: [PATCH 03/16] [Flight] Fix wrong missing key warning when static child is blocked (#34350) --- .../react-client/src/ReactFlightClient.js | 33 ++++++++-- .../__tests__/ReactFlightDOMBrowser-test.js | 60 +++++++++++++++++++ packages/react/src/ReactLazy.js | 2 + packages/react/src/jsx/ReactJSXElement.js | 16 +++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index b966645623..c583e4c84f 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1074,7 +1074,14 @@ function getTaskName(type: mixed): string { } } -function initializeElement(response: Response, element: any): void { +function initializeElement( + response: Response, + element: any, + lazyType: null | LazyComponent< + React$Element, + SomeChunk>, + >, +): void { if (!__DEV__) { return; } @@ -1141,6 +1148,18 @@ function initializeElement(response: Response, element: any): void { if (owner !== null) { initializeFakeStack(response, owner); } + + // In case the JSX runtime has validated the lazy type as a static child, we + // need to transfer this information to the element. + if ( + lazyType && + lazyType._store && + lazyType._store.validated && + !element._store.validated + ) { + element._store.validated = lazyType._store.validated; + } + // TODO: We should be freezing the element but currently, we might write into // _debugInfo later. We could move it into _store which remains mutable. Object.freeze(element.props); @@ -1230,7 +1249,7 @@ function createElement( handler.reason, ); if (__DEV__) { - initializeElement(response, element); + initializeElement(response, element, null); // Conceptually the error happened inside this Element but right before // it was rendered. We don't have a client side component to render but // we can add some DebugInfo to explain that this was conceptually a @@ -1258,16 +1277,17 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = blockedChunk; + const lazyType = createLazyChunkWrapper(blockedChunk); if (__DEV__) { - /// After we have initialized any blocked references, initialize stack etc. - const init = initializeElement.bind(null, response, element); + // After we have initialized any blocked references, initialize stack etc. + const init = initializeElement.bind(null, response, element, lazyType); blockedChunk.then(init, init); } - return createLazyChunkWrapper(blockedChunk); + return lazyType; } } if (__DEV__) { - initializeElement(response, element); + initializeElement(response, element, null); } return element; @@ -1279,6 +1299,7 @@ function createLazyChunkWrapper( const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: chunk, + _store: {validated: 0}, _init: readChunk, }; if (__DEV__) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index cd546f6135..a49e268ebf 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('

Hi

'); }); + + it('should not have missing key warnings when a static child is blocked on debug info', async () => { + const ClientComponent = clientExports(function ClientComponent({element}) { + return ( +
+ Hi + {element} +
+ ); + }); + + let debugReadableStreamController; + + const debugReadableStream = new ReadableStream({ + start(controller) { + debugReadableStreamController = controller; + }, + }); + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + Sebbie} />, + webpackMap, + { + debugChannel: { + writable: new WritableStream({ + write(chunk) { + debugReadableStreamController.enqueue(chunk); + }, + close() { + debugReadableStreamController.close(); + }, + }), + }, + }, + ), + ); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + debugChannel: {readable: createDelayedStream(debugReadableStream)}, + }); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + // Wait for the debug info to be processed. + await act(() => {}); + + expect(container.innerHTML).toBe( + '
HiSebbie
', + ); + }); }); diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 69b35b58cc..3b7f97d6c3 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -60,6 +60,8 @@ export type LazyComponent = { _payload: P, _init: (payload: P) => T, _debugInfo?: null | ReactDebugInfo, + // __DEV__ + _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail }; function lazyInitializer(payload: Payload): T { diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index cb475340c9..a77c4c3cdb 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -804,6 +804,14 @@ function validateChildKeys(node) { if (node._store) { node._store.validated = 1; } + } else if (isLazyType(node)) { + if (node._payload.status === 'fulfilled') { + if (isValidElement(node._payload.value) && node._payload.value._store) { + node._payload.value._store.validated = 1; + } + } else if (node._store) { + node._store.validated = 1; + } } } } @@ -822,3 +830,11 @@ export function isValidElement(object) { object.$$typeof === REACT_ELEMENT_TYPE ); } + +export function isLazyType(object) { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_LAZY_TYPE + ); +} From 1549bda33f0df963ae27a590b7191f3de99dad31 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 1 Sep 2025 12:13:05 +0200 Subject: [PATCH 04/16] [Flight] Only assign `_store` in dev mode when creating lazy types (#34354) Small follow-up to #34350. The `_store` property is now only assigned in development mode when creating lazy types. It also uses the `validated` value that was passed to `createElement`, if applicable. --- packages/react-client/src/ReactFlightClient.js | 12 +++++++----- packages/react/src/ReactLazy.js | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index c583e4c84f..fc59a91fb2 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1172,7 +1172,7 @@ function createElement( props: mixed, owner: ?ReactComponentInfo, // DEV-only stack: ?ReactStackTrace, // DEV-only - validated: number, // DEV-only + validated: 0 | 1 | 2, // DEV-only ): | React$Element | LazyComponent, SomeChunk>> { @@ -1268,7 +1268,7 @@ function createElement( } erroredChunk._debugInfo = [erroredComponent]; } - return createLazyChunkWrapper(erroredChunk); + return createLazyChunkWrapper(erroredChunk, validated); } if (handler.deps > 0) { // We have blocked references inside this Element but we can turn this into @@ -1277,7 +1277,7 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = blockedChunk; - const lazyType = createLazyChunkWrapper(blockedChunk); + const lazyType = createLazyChunkWrapper(blockedChunk, validated); if (__DEV__) { // After we have initialized any blocked references, initialize stack etc. const init = initializeElement.bind(null, response, element, lazyType); @@ -1295,11 +1295,11 @@ function createElement( function createLazyChunkWrapper( chunk: SomeChunk, + validated: 0 | 1 | 2, // DEV-only ): LazyComponent> { const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: chunk, - _store: {validated: 0}, _init: readChunk, }; if (__DEV__) { @@ -1307,6 +1307,8 @@ function createLazyChunkWrapper( const chunkDebugInfo: ReactDebugInfo = chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo)); lazyType._debugInfo = chunkDebugInfo; + // Initialize a store for key validation by the JSX runtime. + lazyType._store = {validated: validated}; } return lazyType; } @@ -2111,7 +2113,7 @@ function parseModelString( } // We create a React.lazy wrapper around any lazy values. // When passed into React, we'll know how to suspend on this. - return createLazyChunkWrapper(chunk); + return createLazyChunkWrapper(chunk, 0); } case '@': { // Promise diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 3b7f97d6c3..55b1690b7c 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -59,8 +59,9 @@ export type LazyComponent = { $$typeof: symbol | number, _payload: P, _init: (payload: P) => T, - _debugInfo?: null | ReactDebugInfo, + // __DEV__ + _debugInfo?: null | ReactDebugInfo, _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail }; From b1b0955f2b34286a7408e58463f4cc429627f9a8 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Mon, 1 Sep 2025 16:40:30 +0200 Subject: [PATCH 05/16] [DevTools] Fix inspected element scroll in Suspense tab (#34355) --- .../src/devtools/views/SuspenseTab/SuspenseTab.css | 9 +++++---- .../src/devtools/views/SuspenseTab/SuspenseTimeline.css | 5 +++++ .../src/devtools/views/SuspenseTab/SuspenseTreeList.js | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css index 936910a3c1..60a7328589 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTab.css @@ -16,14 +16,15 @@ .TreeWrapper { border-top: 1px solid var(--color-border); - flex: 1 1 var(--horizontal-resize-tree-percentage); + flex: 1 1 65%; display: flex; flex-direction: row; height: 100%; + overflow: auto; } .InspectedElementWrapper { - flex: 1 1 35%; + flex: 0 0 calc(100% - var(--horizontal-resize-tree-percentage)); overflow-x: hidden; overflow-y: auto; } @@ -59,12 +60,12 @@ .TreeWrapper { border-top: 1px solid var(--color-border); - flex: 1 1 var(--vertical-resize-tree-percentage); + flex: 1 1 50%; overflow: hidden; } .InspectedElementWrapper { - flex: 1 1 50%; + flex: 0 0 calc(100% - var(--vertical-resize-tree-percentage)); } .TreeWrapper + .ResizeBarWrapper .ResizeBar { diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css index 9e1fa3efad..18a58323e5 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css @@ -9,6 +9,11 @@ display: flex; flex-direction: column; flex-grow: 1; + /* + * `overflow: auto` will add scrollbars but the input will not actually grow beyond visible content. + * `overflow: hidden` will constrain the input to its visible content. + */ + overflow: hidden; } .SuspenseTimelineRootSwitcher { diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeList.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeList.js index e0539fc133..a4a794363e 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeList.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTreeList.js @@ -10,5 +10,5 @@ import * as React from 'react'; export default function SuspenseTreeList(_: {}): React$Node { - return
Activity slices
; + return
Activity slices not implemented yet
; } From 6a58b80020457e2976e3139fb825ce5ed0030dd2 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 2 Sep 2025 12:40:54 +0200 Subject: [PATCH 06/16] [DevTools] Only inspect elements on left mouseclick (#34361) --- .../src/devtools/views/Components/Element.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Element.js b/packages/react-devtools-shared/src/devtools/views/Components/Element.js index 25e5208ce9..00fae0951f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Element.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Element.js @@ -80,8 +80,8 @@ export default function Element({data, index, style}: Props): React.Node { }; // $FlowFixMe[missing-local-annot] - const handleClick = ({metaKey}) => { - if (id !== null) { + const handleClick = ({metaKey, button}) => { + if (id !== null && button === 0) { logEvent({ event_name: 'select-element', metadata: {source: 'click-element'}, From 8e60cb7ed55a3dce35bd809b4cf1ad803c59abfd Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Tue, 2 Sep 2025 14:59:15 +0200 Subject: [PATCH 07/16] [DevTools] Remove markers from Suspense timeline (#34357) --- .../views/SuspenseTab/SuspenseTimeline.css | 20 +----- .../views/SuspenseTab/SuspenseTimeline.js | 72 ++++++------------- 2 files changed, 24 insertions(+), 68 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css index 18a58323e5..6404c32627 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.css @@ -2,7 +2,7 @@ width: 100%; display: flex; flex-direction: row; - padding: 0 0.25rem; + padding: 0.25rem; } .SuspenseTimelineInput { @@ -21,20 +21,6 @@ max-width: 3rem; } -.SuspenseTimelineMarkers { - display: flex; - flex-direction: row; - justify-content: space-between; +.SuspenseTimelineProgressIndicator { + align-self: center; } - -.SuspenseTimelineMarkers > * { - flex: 1 1 0; - overflow: visible; - visibility: hidden; - width: 0 -} - -.SuspenseTimelineActiveMarker { - visibility: visible; -} - 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 78a2bd1135..04916f9a36 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseTimeline.js @@ -11,14 +11,7 @@ import type {Element, SuspenseNode} from '../../../frontend/types'; import type Store from '../../store'; import * as React from 'react'; -import { - useContext, - useId, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; +import {useContext, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {BridgeContext, StoreContext} from '../context'; import {TreeDispatcherContext} from '../Components/TreeContext'; import {useHighlightHostInstance} from '../hooks'; @@ -112,30 +105,6 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) { setValue(max); } - const markersID = useId(); - const markers: React.Node[] = useMemo(() => { - return timeline.map((suspense, index) => { - const takesUpSpace = - suspense.rects !== null && - suspense.rects.some(rect => { - return rect.width > 0 && rect.height > 0; - }); - - return takesUpSpace ? ( - - ) : ( -