diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js index 522d211aeb06f..1136dd29281cb 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElement-test.js @@ -682,6 +682,7 @@ describe('InspectedElement', () => { object_with_symbol={objectWithSymbol} proxy={proxyInstance} react_element={} + react_lazy={React.lazy(async () => ({default: 'foo'}))} regexp={/abc/giu} set={setShallow} set_of_sets={setOfSets} @@ -780,9 +781,18 @@ describe('InspectedElement', () => { "preview_short": () => {}, "preview_long": () => {}, }, - "react_element": Dehydrated { - "preview_short": , - "preview_long": , + "react_element": { + "key": null, + "props": Dehydrated { + "preview_short": {…}, + "preview_long": {}, + }, + }, + "react_lazy": { + "_payload": Dehydrated { + "preview_short": {…}, + "preview_long": {_result: () => {}, _status: -1}, + }, }, "regexp": Dehydrated { "preview_short": /abc/giu, @@ -930,13 +940,13 @@ describe('InspectedElement', () => { const inspectedElement = await inspectElementAtIndex(0); expect(inspectedElement.props).toMatchInlineSnapshot(` - { - "unusedPromise": Dehydrated { - "preview_short": Promise, - "preview_long": Promise, - }, - } - `); + { + "unusedPromise": Dehydrated { + "preview_short": Promise, + "preview_long": Promise, + }, + } + `); }); it('should not consume iterables while inspecting', async () => { diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index cf1ce1ffa3e38..f306ab97093d9 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -289,9 +289,13 @@ describe('InspectedElementContext', () => { "preview_long": {boolean: true, number: 123, string: "abc"}, }, }, - "react_element": Dehydrated { - "preview_short": , - "preview_long": , + "react_element": { + "key": null, + "props": Dehydrated { + "preview_short": {…}, + "preview_long": {}, + }, + "ref": null, }, "regexp": Dehydrated { "preview_short": /abc/giu, diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index 7ce5a8ec6ab38..ecadad7ab3fe2 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -16,6 +16,8 @@ import { setInObject, } from 'react-devtools-shared/src/utils'; +import {REACT_LEGACY_ELEMENT_TYPE} from 'shared/ReactSymbols'; + import type { DehydratedData, InspectedElementPath, @@ -188,18 +190,103 @@ export function dehydrate( type, }; - // React Elements aren't very inspector-friendly, - // and often contain private fields or circular references. - case 'react_element': - cleaned.push(path); - return { - inspectable: false, + case 'react_element': { + isPathAllowedCheck = isPathAllowed(path); + + if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { + cleaned.push(path); + return { + inspectable: true, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), + name: getDisplayNameForReactElement(data) || 'Unknown', + type, + }; + } + + const unserializableValue: Unserializable = { + unserializable: true, + type, + readonly: true, preview_short: formatDataForPreview(data, false), preview_long: formatDataForPreview(data, true), name: getDisplayNameForReactElement(data) || 'Unknown', - type, }; + // TODO: We can't expose type because that name is already taken on Unserializable. + unserializableValue.key = dehydrate( + data.key, + cleaned, + unserializable, + path.concat(['key']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + if (data.$$typeof === REACT_LEGACY_ELEMENT_TYPE) { + unserializableValue.ref = dehydrate( + data.ref, + cleaned, + unserializable, + path.concat(['ref']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + } + unserializableValue.props = dehydrate( + data.props, + cleaned, + unserializable, + path.concat(['props']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + unserializable.push(path); + return unserializableValue; + } + case 'react_lazy': { + isPathAllowedCheck = isPathAllowed(path); + + const payload = data._payload; + + if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { + cleaned.push(path); + const inspectable = + payload !== null && + typeof payload === 'object' && + (payload._status === 1 || + payload._status === 2 || + payload.status === 'fulfilled' || + payload.status === 'rejected'); + return { + inspectable, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), + name: 'lazy()', + type, + }; + } + + const unserializableValue: Unserializable = { + unserializable: true, + type: type, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), + name: 'lazy()', + }; + // Ideally we should alias these properties to something more readable but + // unfortunately because of how the hydration algorithm uses a single concept of + // "path" we can't alias the path. + unserializableValue._payload = dehydrate( + payload, + cleaned, + unserializable, + path.concat(['_payload']), + isPathAllowed, + isPathAllowedCheck ? 1 : level + 1, + ); + unserializable.push(path); + return unserializableValue; + } // ArrayBuffers error if you try to inspect them. case 'array_buffer': case 'data_view': @@ -309,6 +396,7 @@ export function dehydrate( isPathAllowedCheck = isPathAllowed(path); if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { + cleaned.push(path); return { inspectable: data.status === 'fulfilled' || data.status === 'rejected', diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 325224844d746..aac3bd719e183 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -591,6 +591,7 @@ export type DataType = | 'thenable' | 'object' | 'react_element' + | 'react_lazy' | 'regexp' | 'string' | 'symbol' @@ -644,11 +645,12 @@ export function getDataType(data: Object): DataType { return 'number'; } case 'object': - if ( - data.$$typeof === REACT_ELEMENT_TYPE || - data.$$typeof === REACT_LEGACY_ELEMENT_TYPE - ) { - return 'react_element'; + switch (data.$$typeof) { + case REACT_ELEMENT_TYPE: + case REACT_LEGACY_ELEMENT_TYPE: + return 'react_element'; + case REACT_LAZY_TYPE: + return 'react_lazy'; } if (isArray(data)) { return 'array'; @@ -864,6 +866,62 @@ export function formatDataForPreview( return `<${truncateForDisplay( getDisplayNameForReactElement(data) || 'Unknown', )} />`; + case 'react_lazy': + // To avoid actually initialize a lazy to cause a side-effect we make some assumptions + // about the structure of the payload even though that's not really part of the contract. + // In practice, this is really just coming from React.lazy helper or Flight. + const payload = data._payload; + if (payload !== null && typeof payload === 'object') { + if (payload._status === 0) { + // React.lazy constructor pending + return `pending lazy()`; + } + if (payload._status === 1 && payload._result != null) { + // React.lazy constructor fulfilled + if (showFormattedValue) { + const formatted = formatDataForPreview( + payload._result.default, + false, + ); + return `fulfilled lazy() {${truncateForDisplay(formatted)}}`; + } else { + return `fulfilled lazy() {…}`; + } + } + if (payload._status === 2) { + // React.lazy constructor rejected + if (showFormattedValue) { + const formatted = formatDataForPreview(payload._result, false); + return `rejected lazy() {${truncateForDisplay(formatted)}}`; + } else { + return `rejected lazy() {…}`; + } + } + if (payload.status === 'pending' || payload.status === 'blocked') { + // React Flight pending + return `pending lazy()`; + } + if (payload.status === 'fulfilled') { + // React Flight fulfilled + if (showFormattedValue) { + const formatted = formatDataForPreview(payload.value, false); + return `fulfilled lazy() {${truncateForDisplay(formatted)}}`; + } else { + return `fulfilled lazy() {…}`; + } + } + if (payload.status === 'rejected') { + // React Flight rejected + if (showFormattedValue) { + const formatted = formatDataForPreview(payload.reason, false); + return `rejected lazy() {${truncateForDisplay(formatted)}}`; + } else { + return `rejected lazy() {…}`; + } + } + } + // Some form of uninitialized + return 'lazy()'; case 'array_buffer': return `ArrayBuffer(${data.byteLength})`; case 'data_view':