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':