From 326832a56d41b4462919f9efe69916712ca87a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 30 Sep 2024 12:45:13 -0700 Subject: [PATCH 1/6] [Flight] Serialize Error Values (#31104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idea is that the RSC protocol is a superset of Structured Clone. #25687 One exception that we left out was serializing Error objects as values. We serialize "throws" or "rejections" as Error (regardless of their type) but not Error values. This fixes that by serializing `Error` objects. We don't include digest in this case since we don't call `onError` and it's not really expected that you'd log it on the server with some way to look it up. In general this is not super useful outside throws. Especially since we hide their values in prod. However, there is one case where it is quite useful. When you replay console logs in DEV you might often log an Error object within the scope of a Server Component. E.g. the default RSC error handling just console.error and error object. Before this would just be an empty object due to our lax console log serialization: Screenshot 2024-09-30 at 2 24 03 PM After: Screenshot 2024-09-30 at 2 36 48 PM TODO for a follow up: Flight Reply direction. This direction doesn't actually serialize thrown errors because they always reject the serialization. --- .../react-client/src/ReactFlightClient.js | 74 +++++++++---------- .../src/__tests__/ReactFlight-test.js | 40 ++++++++++ .../react-server/src/ReactFlightServer.js | 36 +++++++++ 3 files changed, 112 insertions(+), 38 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 83c509dab46a8..98a52c4ec4a9a 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1287,6 +1287,21 @@ function parseModelString( createFormData, ); } + case 'Z': { + // Error + if (__DEV__) { + const ref = value.slice(2); + return getOutlinedModel( + response, + ref, + parentObject, + key, + resolveErrorDev, + ); + } else { + return resolveErrorProd(response); + } + } case 'i': { // Iterator const ref = value.slice(2); @@ -1881,11 +1896,7 @@ function formatV8Stack( } type ErrorWithDigest = Error & {digest?: string}; -function resolveErrorProd( - response: Response, - id: number, - digest: string, -): void { +function resolveErrorProd(response: Response): Error { if (__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes @@ -1899,25 +1910,17 @@ function resolveErrorProd( ' may provide additional details about the nature of the error.', ); error.stack = 'Error: ' + error.message; - (error: any).digest = digest; - const errorWithDigest: ErrorWithDigest = (error: any); - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(response, errorWithDigest)); - } else { - triggerErrorOnChunk(chunk, errorWithDigest); - } + return error; } function resolveErrorDev( response: Response, - id: number, - digest: string, - message: string, - stack: ReactStackTrace, - env: string, -): void { + errorInfo: {message: string, stack: ReactStackTrace, env: string, ...}, +): Error { + const message: string = errorInfo.message; + const stack: ReactStackTrace = errorInfo.stack; + const env: string = errorInfo.env; + if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes @@ -1957,16 +1960,8 @@ function resolveErrorDev( } } - (error: any).digest = digest; (error: any).environmentName = env; - const errorWithDigest: ErrorWithDigest = (error: any); - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(response, errorWithDigest)); - } else { - triggerErrorOnChunk(chunk, errorWithDigest); - } + return error; } function resolvePostponeProd(response: Response, id: number): void { @@ -2622,17 +2617,20 @@ function processFullStringRow( } case 69 /* "E" */: { const errorInfo = JSON.parse(row); + let error; if (__DEV__) { - resolveErrorDev( - response, - id, - errorInfo.digest, - errorInfo.message, - errorInfo.stack, - errorInfo.env, - ); + error = resolveErrorDev(response, errorInfo); + } else { + error = resolveErrorProd(response); + } + (error: any).digest = errorInfo.digest; + const errorWithDigest: ErrorWithDigest = (error: any); + const chunks = response._chunks; + const chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, createErrorChunk(response, errorWithDigest)); } else { - resolveErrorProd(response, id, errorInfo.digest); + triggerErrorOnChunk(chunk, errorWithDigest); } return; } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 34986dc623de8..27db069ef324f 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -653,6 +653,46 @@ describe('ReactFlight', () => { `); }); + it('can transport Error objects as values', async () => { + function ComponentClient({prop}) { + return ` + is error: ${prop instanceof Error} + message: ${prop.message} + stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')} + environmentName: ${prop.environmentName} + `; + } + const Component = clientReference(ComponentClient); + + function ServerComponent() { + const error = new Error('hello'); + return ; + } + + const transport = ReactNoopFlightServer.render(); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + if (__DEV__) { + expect(ReactNoop).toMatchRenderedOutput(` + is error: true + message: hello + stack: Error: hello + in ServerComponent (at **) + environmentName: Server + `); + } else { + expect(ReactNoop).toMatchRenderedOutput(` + is error: true + message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error. + stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error. + environmentName: undefined + `); + } + }); + it('can transport cyclic objects', async () => { function ComponentClient({prop}) { expect(prop.obj.obj.obj).toBe(prop.obj.obj); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 5d79482de0186..19c40c214b918 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2688,6 +2688,9 @@ function renderModelDestructive( if (typeof FormData === 'function' && value instanceof FormData) { return serializeFormData(request, value); } + if (value instanceof Error) { + return serializeErrorValue(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) { @@ -3114,6 +3117,36 @@ function emitPostponeChunk( request.completedErrorChunks.push(processedChunk); } +function serializeErrorValue(request: Request, error: Error): string { + if (__DEV__) { + let message; + let stack: ReactStackTrace; + let env = (0, request.environmentName)(); + try { + // eslint-disable-next-line react-internal/safe-string-coercion + message = String(error.message); + stack = filterStackTrace(request, error, 0); + const errorEnv = (error: any).environmentName; + if (typeof errorEnv === 'string') { + // This probably came from another FlightClient as a pass through. + // Keep the environment name. + env = errorEnv; + } + } catch (x) { + message = 'An error occurred but serializing the error message failed.'; + stack = []; + } + const errorInfo = {message, stack, env}; + const id = outlineModel(request, errorInfo); + return '$Z' + id.toString(16); + } else { + // In prod we don't emit any information about this Error object to avoid + // unintentional leaks. Since this doesn't actually throw on the server + // we don't go through onError and so don't register any digest neither. + return '$Z'; + } +} + function emitErrorChunk( request: Request, id: number, @@ -3403,6 +3436,9 @@ function renderConsoleValue( if (typeof FormData === 'function' && value instanceof FormData) { return serializeFormData(request, value); } + if (value instanceof Error) { + return serializeErrorValue(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) { From 654e387d7eac113ddbf85f8a9029d1af7117679e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 30 Sep 2024 22:39:20 -0700 Subject: [PATCH 2/6] [Flight] Serialize Server Components Props in DEV (#31105) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to show props in React DevTools when inspecting a Server Component. I currently drastically limit the object depth that's serialized since this is very implicit and you can have heavy objects on the server. We previously was using the general outlineModel to outline ReactComponentInfo but we weren't consistently using it everywhere which could cause some bugs with the parsing when it got deduped on the client. It also lead to the weird feature detect of `isReactComponent`. It also meant that this serialization was using the plain serialization instead of `renderConsoleValue` which means we couldn't safely serialize arbitrary debug info that isn't serializable there. So the main change here is to call `outlineComponentInfo` and have that always write every "Server Component" instance as outlined and in a way that lets its props be serialized using `renderConsoleValue`. Screenshot 2024-10-01 at 1 25 05 AM --- .../react-client/src/ReactFlightClient.js | 21 +- .../src/__tests__/ReactFlight-test.js | 33 +++ .../src/backend/fiber/renderer.js | 3 +- .../react-devtools-shared/src/hydration.js | 58 ++++- .../__tests__/ReactFlightDOMBrowser-test.js | 8 +- .../react-server/src/ReactFlightServer.js | 218 +++++++++++------- packages/shared/ReactTypes.js | 1 + 7 files changed, 245 insertions(+), 97 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 98a52c4ec4a9a..2f60ccddb4b4d 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2640,11 +2640,22 @@ function processFullStringRow( } case 68 /* "D" */: { if (__DEV__) { - const debugInfo: ReactComponentInfo | ReactAsyncInfo = parseModel( - response, - row, - ); - resolveDebugInfo(response, id, debugInfo); + const chunk: ResolvedModelChunk = + createResolvedModelChunk(response, row); + initializeModelChunk(chunk); + const initializedChunk: SomeChunk = + chunk; + if (initializedChunk.status === INITIALIZED) { + resolveDebugInfo(response, id, initializedChunk.value); + } else { + // TODO: This is not going to resolve in the right order if there's more than one. + chunk.then( + v => resolveDebugInfo(response, id, v), + e => { + // Ignore debug info errors for now. Unnecessary noise. + }, + ); + } return; } // Fallthrough to share the error with Console entries. diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 27db069ef324f..857ce99868d9b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -308,6 +308,10 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + lastName: 'Smith', + }, }, ] : undefined, @@ -347,6 +351,10 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + lastName: 'Smith', + }, }, ] : undefined, @@ -2665,6 +2673,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + transport: expect.arrayContaining([]), + }, }, ] : undefined, @@ -2683,6 +2694,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2698,6 +2710,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in myLazy (at **)\n in lazyInitializer (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2713,6 +2726,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2787,6 +2801,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + transport: expect.arrayContaining([]), + }, }, ] : undefined, @@ -2804,6 +2821,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in ServerComponent (at **)' : undefined, + props: { + children: {}, + }, }, ] : undefined, @@ -2820,6 +2840,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2978,6 +2999,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, { env: 'B', @@ -3108,6 +3130,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + }, }; expect(getDebugInfo(greeting)).toEqual([ greetInfo, @@ -3119,6 +3144,14 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Greeting (at **)' : undefined, + props: { + children: expect.objectContaining({ + type: 'span', + props: { + children: ['Hello, ', 'Seb'], + }, + }), + }, }, ]); // The owner that created the span was the outer server component. diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 4fac8839ae799..9732bf105ca6c 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -4348,8 +4348,7 @@ export function attach( const componentInfo = virtualInstance.data; const key = typeof componentInfo.key === 'string' ? componentInfo.key : null; - const props = null; // TODO: Track props on ReactComponentInfo; - + const props = componentInfo.props == null ? null : componentInfo.props; const owners: null | Array = getOwnersListFromInstance(virtualInstance); diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index c5b78135e74a2..c21efe40a88fa 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -216,16 +216,19 @@ export function dehydrate( if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { return createDehydrated(type, true, data, cleaned, path); } - return data.map((item, i) => - dehydrate( - item, + const arr: Array = []; + for (let i = 0; i < data.length; i++) { + arr[i] = dehydrateKey( + data, + i, cleaned, unserializable, path.concat([i]), isPathAllowed, isPathAllowedCheck ? 1 : level + 1, - ), - ); + ); + } + return arr; case 'html_all_collection': case 'typed_array': @@ -311,8 +314,9 @@ export function dehydrate( } = {}; getAllEnumerableKeys(data).forEach(key => { const name = key.toString(); - object[name] = dehydrate( - data[key], + object[name] = dehydrateKey( + data, + key, cleaned, unserializable, path.concat([name]), @@ -373,6 +377,46 @@ export function dehydrate( } } +function dehydrateKey( + parent: Object, + key: number | string | symbol, + cleaned: Array>, + unserializable: Array>, + path: Array, + isPathAllowed: (path: Array) => boolean, + level: number = 0, +): $PropertyType { + try { + return dehydrate( + parent[key], + cleaned, + unserializable, + path, + isPathAllowed, + level, + ); + } catch (error) { + let preview = ''; + if ( + typeof error === 'object' && + error !== null && + typeof error.stack === 'string' + ) { + preview = error.stack; + } else if (typeof error === 'string') { + preview = error; + } + cleaned.push(path); + return { + inspectable: false, + preview_short: '[Exception]', + preview_long: preview ? '[Exception: ' + preview + ']' : '[Exception]', + name: preview, + type: 'unknown', + }; + } +} + export function fillInPath( object: Object, data: DehydratedData, 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 0408a63fc512f..882c0bbb01969 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -709,7 +709,7 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe(expectedHtml); if (__DEV__) { - const resolvedPath1b = await response.value[0].props.children[1]._payload; + const resolvedPath1b = response.value[0].props.children[1]; expect(resolvedPath1b._owner).toEqual( expect.objectContaining({ @@ -1028,8 +1028,10 @@ describe('ReactFlightDOMBrowser', () => { expect(flightResponse).toContain('(loading everything)'); expect(flightResponse).toContain('(loading sidebar)'); expect(flightResponse).toContain('(loading posts)'); - expect(flightResponse).not.toContain(':friends:'); - expect(flightResponse).not.toContain(':name:'); + if (!__DEV__) { + expect(flightResponse).not.toContain(':friends:'); + expect(flightResponse).not.toContain(':name:'); + } await serverAct(() => { resolveFriends(); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 19c40c214b918..5db03d628146f 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1148,14 +1148,20 @@ function renderFunctionComponent( ? null : filterStackTrace(request, task.debugStack, 1); // $FlowFixMe[cannot-write] + componentDebugInfo.props = props; + // $FlowFixMe[cannot-write] componentDebugInfo.debugStack = task.debugStack; // $FlowFixMe[cannot-write] componentDebugInfo.debugTask = task.debugTask; + } else { + // $FlowFixMe[cannot-write] + componentDebugInfo.props = props; } // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up // being no references to this as an owner. - outlineModel(request, componentDebugInfo); + + outlineComponentInfo(request, componentDebugInfo); emitDebugChunk(request, componentDebugID, componentDebugInfo); // We've emitted the latest environment for this task so we track that. @@ -1582,6 +1588,13 @@ function renderClientElement( } else if (keyPath !== null) { key = keyPath + ',' + key; } + if (__DEV__) { + if (task.debugOwner !== null) { + // Ensure we outline this owner if it is the first time we see it. + // So that we can refer to it directly. + outlineComponentInfo(request, task.debugOwner); + } + } const element = __DEV__ ? enableOwnerStacks ? [ @@ -1702,6 +1715,7 @@ function renderElement( task.debugStack === null ? null : filterStackTrace(request, task.debugStack, 1), + props: props, debugStack: task.debugStack, debugTask: task.debugTask, }; @@ -2128,7 +2142,7 @@ function serializeSet(request: Request, set: Set): string { function serializeConsoleMap( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, map: Map, ): string { // Like serializeMap but for renderConsoleValue. @@ -2139,7 +2153,7 @@ function serializeConsoleMap( function serializeConsoleSet( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, set: Set, ): string { // Like serializeMap but for renderConsoleValue. @@ -2263,23 +2277,6 @@ function escapeStringValue(value: string): string { } } -function isReactComponentInfo(value: any): boolean { - // TODO: We don't currently have a brand check on ReactComponentInfo. Reconsider. - return ( - ((typeof value.debugTask === 'object' && - value.debugTask !== null && - // $FlowFixMe[method-unbinding] - typeof value.debugTask.run === 'function') || - value.debugStack instanceof Error) && - (enableOwnerStacks - ? isArray((value: any).stack) || (value: any).stack === null - : typeof (value: any).stack === 'undefined') && - typeof value.name === 'string' && - typeof value.env === 'string' && - value.owner !== undefined - ); -} - let modelRoot: null | ReactClientValue = false; function renderModel( @@ -2795,25 +2792,6 @@ function renderModelDestructive( ); } if (__DEV__) { - if (isReactComponentInfo(value)) { - // This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we - // need to omit it before serializing. - const componentDebugInfo: Omit< - ReactComponentInfo, - 'debugTask' | 'debugStack', - > = { - name: (value: any).name, - env: (value: any).env, - key: (value: any).key, - owner: (value: any).owner, - }; - if (enableOwnerStacks) { - // $FlowFixMe[cannot-write] - componentDebugInfo.stack = (value: any).stack; - } - return componentDebugInfo; - } - if (objectName(value) !== 'Object') { callWithDebugContextInDEV(request, task, () => { console.error( @@ -3241,7 +3219,7 @@ function emitDebugChunk( // We use the console encoding so that we can dedupe objects but don't necessarily // use the full serialization that requires a task. - const counter = {objectCount: 0}; + const counter = {objectLimit: 500}; function replacer( this: | {+[key: string | number]: ReactClientValue} @@ -3265,6 +3243,61 @@ function emitDebugChunk( request.completedRegularChunks.push(processedChunk); } +function outlineComponentInfo( + request: Request, + componentInfo: ReactComponentInfo, +): void { + if (!__DEV__) { + // These errors should never make it into a build so we don't need to encode them in codes.json + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'outlineComponentInfo should never be called in production mode. This is a bug in React.', + ); + } + + if (request.writtenObjects.has(componentInfo)) { + // Already written + return; + } + + if (componentInfo.owner != null) { + // Ensure the owner is already outlined. + outlineComponentInfo(request, componentInfo.owner); + } + + // Limit the number of objects we write to prevent emitting giant props objects. + let objectLimit = 10; + if (componentInfo.stack != null) { + // Ensure we have enough object limit to encode the stack trace. + objectLimit += componentInfo.stack.length; + } + + // We use the console encoding so that we can dedupe objects but don't necessarily + // use the full serialization that requires a task. + const counter = {objectLimit}; + + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing. + const componentDebugInfo: Omit< + ReactComponentInfo, + 'debugTask' | 'debugStack', + > = { + name: componentInfo.name, + env: componentInfo.env, + key: componentInfo.key, + owner: componentInfo.owner, + }; + if (enableOwnerStacks) { + // $FlowFixMe[cannot-write] + componentDebugInfo.stack = componentInfo.stack; + } + // Ensure we serialize props after the stack to favor the stack being complete. + // $FlowFixMe[cannot-write] + componentDebugInfo.props = componentInfo.props; + + const id = outlineConsoleValue(request, counter, componentDebugInfo); + request.writtenObjects.set(componentInfo, serializeByValueID(id)); +} + function emitTypedArrayChunk( request: Request, id: number, @@ -3322,7 +3355,7 @@ function serializeEval(source: string): string { // in the depth it can encode. function renderConsoleValue( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, parent: | {+[propertyName: string | number]: ReactClientValue} | $ReadOnlyArray, @@ -3366,23 +3399,64 @@ function renderConsoleValue( } } - if (counter.objectCount > 500) { + const writtenObjects = request.writtenObjects; + const existingReference = writtenObjects.get(value); + if (existingReference !== undefined) { + // We've already emitted this as a real object, so we can + // just refer to that by its existing reference. + return existingReference; + } + + if (counter.objectLimit <= 0) { // We've reached our max number of objects to serialize across the wire so we serialize this // as a marker so that the client can error when this is accessed by the console. return serializeLimitedObject(); } - counter.objectCount++; + counter.objectLimit--; - const writtenObjects = request.writtenObjects; - const existingReference = writtenObjects.get(value); - // $FlowFixMe[method-unbinding] - if (typeof value.then === 'function') { - if (existingReference !== undefined) { - // We've seen this promise before, so we can just refer to the same result. - return existingReference; + switch ((value: any).$$typeof) { + case REACT_ELEMENT_TYPE: { + const element: ReactElement = (value: any); + + if (element._owner != null) { + outlineComponentInfo(request, element._owner); + } + if (enableOwnerStacks) { + let debugStack: null | ReactStackTrace = null; + if (element._debugStack != null) { + // Outline the debug stack so that it doesn't get cut off. + debugStack = filterStackTrace(request, element._debugStack, 1); + const stackId = outlineConsoleValue( + request, + {objectLimit: debugStack.length + 2}, + debugStack, + ); + request.writtenObjects.set(debugStack, serializeByValueID(stackId)); + } + return [ + REACT_ELEMENT_TYPE, + element.type, + element.key, + element.props, + element._owner, + debugStack, + element._store.validated, + ]; + } + + return [ + REACT_ELEMENT_TYPE, + element.type, + element.key, + element.props, + element._owner, + ]; } + } + // $FlowFixMe[method-unbinding] + if (typeof value.then === 'function') { const thenable: Thenable = (value: any); switch (thenable.status) { case 'fulfilled': { @@ -3416,12 +3490,6 @@ function renderConsoleValue( return serializeInfinitePromise(); } - if (existingReference !== undefined) { - // We've already emitted this as a real object, so we can - // just refer to that by its existing reference. - return existingReference; - } - if (isArray(value)) { return value; } @@ -3503,25 +3571,6 @@ function renderConsoleValue( return Array.from((value: any)); } - if (isReactComponentInfo(value)) { - // This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we - // need to omit it before serializing. - const componentDebugInfo: Omit< - ReactComponentInfo, - 'debugTask' | 'debugStack', - > = { - name: (value: any).name, - env: (value: any).env, - key: (value: any).key, - owner: (value: any).owner, - }; - if (enableOwnerStacks) { - // $FlowFixMe[cannot-write] - componentDebugInfo.stack = (value: any).stack; - } - return componentDebugInfo; - } - // $FlowFixMe[incompatible-return] return value; } @@ -3602,7 +3651,7 @@ function renderConsoleValue( function outlineConsoleValue( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, model: ReactClientValue, ): number { if (!__DEV__) { @@ -3629,7 +3678,9 @@ function outlineConsoleValue( value, ); } catch (x) { - return 'unknown value'; + return ( + 'Unknown Value: React could not send it from the server.\n' + x.message + ); } } @@ -3660,7 +3711,7 @@ function emitConsoleChunk( ); } - const counter = {objectCount: 0}; + const counter = {objectLimit: 500}; function replacer( this: | {+[key: string | number]: ReactClientValue} @@ -3677,10 +3728,17 @@ function emitConsoleChunk( value, ); } catch (x) { - return 'unknown value'; + return ( + 'Unknown Value: React could not send it from the server.\n' + x.message + ); } } + // Ensure the owner is already outlined. + if (owner != null) { + outlineComponentInfo(request, owner); + } + // TODO: Don't double badge if this log came from another Flight Client. const env = (0, request.environmentName)(); const payload = [methodName, stackTrace, owner, env]; @@ -3704,7 +3762,7 @@ function forwardDebugInfo( // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up // being no references to this as an owner. - outlineModel(request, debugInfo[i]); + outlineComponentInfo(request, (debugInfo[i]: any)); } emitDebugChunk(request, id, debugInfo[i]); } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 7d51fbe4725d3..54eccd5538dd8 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -193,6 +193,7 @@ export type ReactComponentInfo = { +key?: null | string, +owner?: null | ReactComponentInfo, +stack?: null | ReactStackTrace, + +props?: null | {[name: string]: mixed}, // Stashed Data for the Specific Execution Environment. Not part of the transport protocol +debugStack?: null | Error, +debugTask?: null | ConsoleTask, From 40357fe63071950b0bba304657a003755aec4e30 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 1 Oct 2024 14:03:48 +0100 Subject: [PATCH 3/6] fix[react-devtools]: request hook initialization inside http server response (#31102) Fixes https://github.com/facebook/react/issues/31100. There are 2 things: 1. In https://github.com/facebook/react/pull/30987, we've introduced a breaking change: importing `react-devtools-core` is no longer enough for installing React DevTools global Hook. You need to call `initialize`, in which you may provide initial settings. I am not adding settings here, because it is not implemented, and there are no plans for supporting this. 2. Calling `installHook` is not necessary inside `standalone.js`, because this script is running inside Electron wrapper (which is just a UI, not the app that we are debugging). We will loose the ability to use React DevTools on this React application, but I guess thats fine. --- packages/react-devtools-core/src/standalone.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index bfd05cf5227ee..d65b41b478fa2 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -17,7 +17,6 @@ import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDev import {Server} from 'ws'; import {join} from 'path'; import {readFileSync} from 'fs'; -import {installHook} from 'react-devtools-shared/src/hook'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import {doesFilePathExist, launchEditor} from './editor'; import { @@ -29,8 +28,6 @@ import {localStorageSetItem} from 'react-devtools-shared/src/storage'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {Source} from 'react-devtools-shared/src/shared/types'; -installHook(window); - export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error'; export type StatusListener = (message: string, status: StatusTypes) => void; export type OnDisconnectedCallback = () => void; @@ -371,9 +368,12 @@ function startServer( '\n;' + backendFile.toString() + '\n;' + + 'ReactDevToolsBackend.initialize();' + + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' - }});`, + }}); + `, ); }); From 9ea5ffa9cba4869474a1d5d53e7d6c135be6adf7 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 1 Oct 2024 14:04:05 +0100 Subject: [PATCH 4/6] chore[react-devtools]: add legacy mode error message to the ignore list for tests (#31060) Without this, the console gets spammy whenever we run React DevTools tests against React 18.x, where this deprecation message was added. --- packages/react-devtools-shared/src/__tests__/setupTests.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index d4afd05899a56..0a821345fcbac 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -150,7 +150,10 @@ function patchConsoleForTestingBeforeHookInstallation() { firstArg.startsWith( 'The current testing environment is not configured to support act', ) || - firstArg.startsWith('You seem to have overlapping act() calls')) + firstArg.startsWith('You seem to have overlapping act() calls') || + firstArg.startsWith( + 'ReactDOM.render is no longer supported in React 18.', + )) ) { // DevTools intentionally wraps updates with acts from both DOM and test-renderer, // since test updates are expected to impact both renderers. From 6e612587ecfca0ea2e331300635d497d54437930 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Tue, 1 Oct 2024 14:26:12 +0100 Subject: [PATCH 5/6] chore[react-devtools]: drop legacy context tests (#31059) We've dropped the support for detecting changes in legacy Contexts in https://github.com/facebook/react/pull/30896. --- .../src/__tests__/profilingCache-test.js | 688 ------------------ 1 file changed, 688 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index 0d6d8d02a1989..fb1fa6c5f2298 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -13,7 +13,6 @@ import type Store from 'react-devtools-shared/src/devtools/store'; import {getVersionedRenderImplementation} from './utils'; describe('ProfilingCache', () => { - let PropTypes; let React; let ReactDOM; let ReactDOMClient; @@ -34,7 +33,6 @@ describe('ProfilingCache', () => { store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; - PropTypes = require('prop-types'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -309,692 +307,6 @@ describe('ProfilingCache', () => { }); }); - // @reactVersion >= 16.9 - // @reactVersion <= 18.2 - it('should record changed props/state/context/hooks for React version [16.9; 18.2] with legacy context', () => { - let instance = null; - - const ModernContext = React.createContext(0); - - class LegacyContextProvider extends React.Component { - static childContextTypes = { - count: PropTypes.number, - }; - state = {count: 0}; - getChildContext() { - return this.state; - } - render() { - instance = this; - return ( - - - - - - - ); - } - } - - const FunctionComponentWithHooks = ({count}) => { - React.useMemo(() => count, [count]); - return null; - }; - - class ModernContextConsumer extends React.Component { - static contextType = ModernContext; - render() { - return ; - } - } - - class LegacyContextConsumer extends React.Component { - static contextTypes = { - count: PropTypes.number, - }; - render() { - return ; - } - } - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => render()); - expect(instance).not.toBeNull(); - utils.act(() => (instance: any).setState({count: 1})); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => store.profilerStore.stopProfiling()); - - const rootID = store.roots[0]; - - let changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - expect(changeDescriptions[0]).toMatchInlineSnapshot(` - Map { - 2 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 4 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 5 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 6 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - } - `); - expect(changeDescriptions[1]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "count", - ], - "state": null, - }, - 4 => { - "context": true, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "count", - ], - "state": null, - }, - 6 => { - "context": [ - "count", - ], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": [ - "count", - ], - }, - } - `); - expect(changeDescriptions[2]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "foo", - ], - "state": [], - }, - } - `); - expect(changeDescriptions[3]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "foo", - "bar", - ], - "state": [], - }, - } - `); - expect(changeDescriptions[4]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "bar", - ], - "state": [], - }, - } - `); - - utils.exportImportHelper(bridge, store); - - const prevChangeDescriptions = [...changeDescriptions]; - - changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - - for (let commitIndex = 0; commitIndex < 5; commitIndex++) { - expect(changeDescriptions[commitIndex]).toEqual( - prevChangeDescriptions[commitIndex], - ); - } - }); - - // @reactVersion > 18.2 - // @gate !disableLegacyContext - it('should record changed props/state/context/hooks for React version (18.2; ∞) with legacy context enabled', () => { - let instance = null; - - const ModernContext = React.createContext(0); - - class LegacyContextProvider extends React.Component { - static childContextTypes = { - count: PropTypes.number, - }; - state = {count: 0}; - getChildContext() { - return this.state; - } - render() { - instance = this; - return ( - - - - - - - ); - } - } - - const FunctionComponentWithHooks = ({count}) => { - React.useMemo(() => count, [count]); - return null; - }; - - class ModernContextConsumer extends React.Component { - static contextType = ModernContext; - render() { - return ; - } - } - - class LegacyContextConsumer extends React.Component { - static contextTypes = { - count: PropTypes.number, - }; - render() { - return ; - } - } - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => render()); - expect(instance).not.toBeNull(); - utils.act(() => (instance: any).setState({count: 1})); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => store.profilerStore.stopProfiling()); - - const rootID = store.roots[0]; - - let changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - expect(changeDescriptions[0]).toEqual( - new Map([ - [ - 2, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 4, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 5, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 6, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - ]), - ); - - expect(changeDescriptions[1]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['count'], - state: null, - }, - ], - [ - 4, - { - context: true, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['count'], - state: null, - }, - ], - [ - 6, - { - context: ['count'], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: ['count'], - }, - ], - ]), - ); - - expect(changeDescriptions[2]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['foo'], - state: [], - }, - ], - ]), - ); - - expect(changeDescriptions[3]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['foo', 'bar'], - state: [], - }, - ], - ]), - ); - - expect(changeDescriptions[4]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['bar'], - state: [], - }, - ], - ]), - ); - - utils.exportImportHelper(bridge, store); - - const prevChangeDescriptions = [...changeDescriptions]; - - changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - - for (let commitIndex = 0; commitIndex < 5; commitIndex++) { - expect(changeDescriptions[commitIndex]).toEqual( - prevChangeDescriptions[commitIndex], - ); - } - }); - // @reactVersion >= 18.0 it('should properly detect changed hooks', () => { const Context = React.createContext(0); From d8c90fa48d3addefe4b805ec56a3c65e4ee39127 Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Tue, 1 Oct 2024 11:00:57 -0400 Subject: [PATCH 6/6] Disable infinite render loop detection (#31088) We're seeing issues with this feature internally including bugs with sibling prerendering and errors that are difficult for developers to action on. We'll turn off the feature for the time being until we can improve the stability and ergonomics. This PR does two things: - Turn off `enableInfiniteLoopDetection` everywhere while leaving it as a variant on www so we can do further experimentation. - Revert https://github.com/facebook/react/pull/31061 which was a temporary change for debugging. This brings the feature back to baseline. --- .../src/__tests__/ReactLegacyUpdates-test.js | 2 +- .../react-dom/src/__tests__/ReactUpdates-test.js | 2 +- packages/react-reconciler/src/ReactFiberWorkLoop.js | 4 ++-- packages/shared/ReactFeatureFlags.js | 12 ++++++------ packages/shared/forks/ReactFeatureFlags.native-fb.js | 2 +- .../shared/forks/ReactFeatureFlags.native-oss.js | 2 +- .../ReactFeatureFlags.test-renderer.native-fb.js | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js index 57f2acfa53465..26f56a938c551 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js @@ -1427,7 +1427,7 @@ describe('ReactLegacyUpdates', () => { } } - let limit = 105; + let limit = 55; await expect(async () => { await act(() => { ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 247a53531c659..faf4b29551350 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1542,7 +1542,7 @@ describe('ReactUpdates', () => { } } - let limit = 105; + let limit = 55; const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index ec5484c4b6eca..196859f6fa7ba 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -608,13 +608,13 @@ let pendingPassiveEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; // Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 100; +const NESTED_UPDATE_LIMIT = 50; let nestedUpdateCount: number = 0; let rootWithNestedUpdates: FiberRoot | null = null; let isFlushingPassiveEffects = false; let didScheduleUpdateDuringPassiveEffects = false; -const NESTED_PASSIVE_UPDATE_LIMIT = 100; +const NESTED_PASSIVE_UPDATE_LIMIT = 50; let nestedPassiveUpdateCount: number = 0; let rootWithPassiveNestedUpdates: FiberRoot | null = null; diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 9935784b533d2..cd9bfedacabd5 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -157,6 +157,12 @@ export const retryLaneExpirationMs = 5000; export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; +/** + * Enables a new error detection for infinite render loops from updates caused + * by setState or similar outside of the component owning the state. + */ +export const enableInfiniteRenderLoopDetection = false; + // ----------------------------------------------------------------------------- // Ready for next major. // @@ -204,12 +210,6 @@ export const enableFilterEmptyStringAttributesDOM = true; // Disabled caching behavior of `react/cache` in client runtimes. export const disableClientCache = true; -/** - * Enables a new error detection for infinite render loops from updates caused - * by setState or similar outside of the component owning the state. - */ -export const enableInfiniteRenderLoopDetection = true; - // Subtle breaking changes to JSX runtime to make it faster, like passing `ref` // as a normal prop instead of stripping it from the props object. diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 6997bf5f6805e..9dcc0f5d033ed 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -60,7 +60,7 @@ export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = true; export const enableHalt = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableContextProfiling = false; export const enableLazyContextPropagation = true; export const enableLegacyCache = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 729cdef96d2dd..741b44daf7926 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -51,7 +51,7 @@ export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableHalt = false; export const enableHiddenSubtreeInsertionEffectCleanup = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableLegacyCache = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index e0d946e54ec00..0cb1497eddb5f 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -41,7 +41,7 @@ export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableHalt = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableHiddenSubtreeInsertionEffectCleanup = true;