React DevTools: Show symbols used as keys in state (#19786)

Co-authored-by: Brian Vaughn <bvaughn@fb.com>
This commit is contained in:
6h057 2020-09-14 15:55:19 +02:00 committed by GitHub
parent 11ee82df45
commit 917cb01a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 415 additions and 8 deletions

View File

@ -208,6 +208,70 @@
return <ChildComponent customObject={new Custom()} />; return <ChildComponent customObject={new Custom()} />;
} }
const baseInheritedKeys = Object.create(Object.prototype, {
enumerableStringBase: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('enumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableStringBase: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('nonEnumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
});
const inheritedKeys = Object.create(baseInheritedKeys, {
enumerableString: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableString: {
value: 3,
writable: true,
enumerable: false,
configurable: true,
},
123: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('nonEnumerableSymbol')]: {
value: 2,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('enumerableSymbol')]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
});
function InheritedKeys() {
return <ChildComponent data={inheritedKeys} />;
}
const object = { const object = {
string: "abc", string: "abc",
longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890", longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
@ -294,6 +358,7 @@
<ObjectProps /> <ObjectProps />
<UnserializableProps /> <UnserializableProps />
<CustomObject /> <CustomObject />
<InheritedKeys />
</Fragment> </Fragment>
); );
} }

View File

@ -541,6 +541,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
"object_of_objects": { "object_of_objects": {
"inner": {} "inner": {}
}, },
"object_with_symbol": {
"Symbol(name)": "hello"
},
"proxy": {}, "proxy": {},
"react_element": {}, "react_element": {},
"regexp": {}, "regexp": {},
@ -612,6 +615,25 @@ exports[`InspectedElementContext should support objects with overridden hasOwnPr
} }
`; `;
exports[`InspectedElementContext should support objects with with inherited keys: 1: Inspected element 2 1`] = `
{
"id": 2,
"owners": null,
"context": null,
"hooks": null,
"props": {
"object": {
"123": 3,
"enumerableString": 2,
"Symbol(enumerableSymbol)": 3,
"enumerableStringBase": 1,
"Symbol(enumerableSymbolBase)": 1
}
},
"state": null
}
`;
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = ` exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
{ {
"id": 2, "id": 2,

View File

@ -537,6 +537,9 @@ describe('InspectedElementContext', () => {
const objectOfObjects = { const objectOfObjects = {
inner: {string: 'abc', number: 123, boolean: true}, inner: {string: 'abc', number: 123, boolean: true},
}; };
const objectWithSymbol = {
[Symbol('name')]: 'hello',
};
const typedArray = Int8Array.from([100, -100, 0]); const typedArray = Int8Array.from([100, -100, 0]);
const arrayBuffer = typedArray.buffer; const arrayBuffer = typedArray.buffer;
const dataView = new DataView(arrayBuffer); const dataView = new DataView(arrayBuffer);
@ -580,6 +583,7 @@ describe('InspectedElementContext', () => {
map={mapShallow} map={mapShallow}
map_of_maps={mapOfMaps} map_of_maps={mapOfMaps}
object_of_objects={objectOfObjects} object_of_objects={objectOfObjects}
object_with_symbol={objectWithSymbol}
proxy={proxyInstance} proxy={proxyInstance}
react_element={<span />} react_element={<span />}
regexp={/abc/giu} regexp={/abc/giu}
@ -633,6 +637,7 @@ describe('InspectedElementContext', () => {
map, map,
map_of_maps, map_of_maps,
object_of_objects, object_of_objects,
object_with_symbol,
proxy, proxy,
react_element, react_element,
regexp, regexp,
@ -737,6 +742,8 @@ describe('InspectedElementContext', () => {
); );
expect(object_of_objects.inner[meta.preview_short]).toBe('{…}'); expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
expect(object_with_symbol['Symbol(name)']).toBe('hello');
expect(proxy[meta.inspectable]).toBe(false); expect(proxy[meta.inspectable]).toBe(false);
expect(proxy[meta.name]).toBe('function'); expect(proxy[meta.name]).toBe('function');
expect(proxy[meta.type]).toBe('function'); expect(proxy[meta.type]).toBe('function');
@ -939,6 +946,111 @@ describe('InspectedElementContext', () => {
done(); done();
}); });
it('should support objects with with inherited keys', async done => {
const Example = () => null;
const base = Object.create(Object.prototype, {
enumerableStringBase: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('enumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableStringBase: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('nonEnumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
});
const object = Object.create(base, {
enumerableString: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableString: {
value: 3,
writable: true,
enumerable: false,
configurable: true,
},
[123]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('nonEnumerableSymbol')]: {
value: 2,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('enumerableSymbol')]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
});
const container = document.createElement('div');
await utils.actAsync(() =>
ReactDOM.render(<Example object={object} />, container),
);
const id = ((store.getElementIDAtIndex(0): any): number);
let inspectedElement = null;
function Suspender({target}) {
const {getInspectedElement} = React.useContext(InspectedElementContext);
inspectedElement = getInspectedElement(id);
return null;
}
await utils.actAsync(
() =>
TestRenderer.create(
<Contexts
defaultSelectedElementID={id}
defaultSelectedElementIndex={0}>
<React.Suspense fallback={null}>
<Suspender target={id} />
</React.Suspense>
</Contexts>,
),
false,
);
expect(inspectedElement).not.toBeNull();
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
expect(inspectedElement.props.object).toEqual({
123: 3,
'Symbol(enumerableSymbol)': 3,
'Symbol(enumerableSymbolBase)': 1,
enumerableString: 2,
enumerableStringBase: 1,
});
done();
});
it('should not dehydrate nested values until explicitly requested', async done => { it('should not dehydrate nested values until explicitly requested', async done => {
const Example = () => { const Example = () => {
const [state] = React.useState({ const [state] = React.useState({

View File

@ -236,6 +236,29 @@ Object {
} }
`; `;
exports[`InspectedElementContext should support objects with with inherited keys: 1: Initial inspection 1`] = `
Object {
"id": 2,
"type": "full-data",
"value": {
"id": 2,
"owners": null,
"context": {},
"hooks": null,
"props": {
"data": {
"123": 3,
"enumerableString": 2,
"Symbol(enumerableSymbol)": 3,
"enumerableStringBase": 1,
"Symbol(enumerableSymbolBase)": 1
}
},
"state": null
},
}
`;
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = ` exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
Object { Object {
"id": 2, "id": 2,

View File

@ -432,6 +432,81 @@ describe('InspectedElementContext', () => {
done(); done();
}); });
it('should support objects with with inherited keys', async done => {
const Example = () => null;
const base = Object.create(Object.prototype, {
enumerableStringBase: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('enumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableStringBase: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('nonEnumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
});
const object = Object.create(base, {
enumerableString: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableString: {
value: 3,
writable: true,
enumerable: false,
configurable: true,
},
[123]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('nonEnumerableSymbol')]: {
value: 2,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('enumerableSymbol')]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
});
act(() =>
ReactDOM.render(<Example data={object} />, document.createElement('div')),
);
const id = ((store.getElementIDAtIndex(0): any): number);
const inspectedElement = await read(id);
expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
done();
});
it('should not dehydrate nested values until explicitly requested', async done => { it('should not dehydrate nested values until explicitly requested', async done => {
const Example = () => null; const Example = () => null;

View File

@ -10,6 +10,7 @@
import { import {
getDataType, getDataType,
getDisplayNameForReactElement, getDisplayNameForReactElement,
getAllEnumerableKeys,
getInObject, getInObject,
formatDataForPreview, formatDataForPreview,
setInObject, setInObject,
@ -291,16 +292,17 @@ export function dehydrate(
return createDehydrated(type, true, data, cleaned, path); return createDehydrated(type, true, data, cleaned, path);
} else { } else {
const object = {}; const object = {};
for (const name in data) { getAllEnumerableKeys(data).forEach(key => {
const name = key.toString();
object[name] = dehydrate( object[name] = dehydrate(
data[name], data[key],
cleaned, cleaned,
unserializable, unserializable,
path.concat([name]), path.concat([name]),
isPathAllowed, isPathAllowed,
isPathAllowedCheck ? 1 : level + 1, isPathAllowedCheck ? 1 : level + 1,
); );
} });
return object; return object;
} }

View File

@ -52,16 +52,41 @@ const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
// Try to reuse the already encoded strings. // Try to reuse the already encoded strings.
const encodedStringCache = new LRU({max: 1000}); const encodedStringCache = new LRU({max: 1000});
export function alphaSortKeys(a: string, b: string): number { export function alphaSortKeys(
if (a > b) { a: string | number | Symbol,
b: string | number | Symbol,
): number {
if (a.toString() > b.toString()) {
return 1; return 1;
} else if (b > a) { } else if (b.toString() > a.toString()) {
return -1; return -1;
} else { } else {
return 0; return 0;
} }
} }
export function getAllEnumerableKeys(
obj: Object,
): Array<string | number | Symbol> {
const keys = [];
let current = obj;
while (current != null) {
const currentKeys = [
...Object.keys(current),
...Object.getOwnPropertySymbols(current),
];
const descriptors = Object.getOwnPropertyDescriptors(current);
currentKeys.forEach(key => {
// $FlowFixMe: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
if (descriptors[key].enumerable) {
keys.push(key);
}
});
current = Object.getPrototypeOf(current);
}
return keys;
}
export function getDisplayName( export function getDisplayName(
type: Function, type: Function,
fallbackName: string = 'Anonymous', fallbackName: string = 'Anonymous',
@ -657,7 +682,7 @@ export function formatDataForPreview(
return data.toString(); return data.toString();
case 'object': case 'object':
if (showFormattedValue) { if (showFormattedValue) {
const keys = Object.keys(data).sort(alphaSortKeys); const keys = getAllEnumerableKeys(data).sort(alphaSortKeys);
let formatted = ''; let formatted = '';
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
@ -665,7 +690,10 @@ export function formatDataForPreview(
if (i > 0) { if (i > 0) {
formatted += ', '; formatted += ', ';
} }
formatted += `${key}: ${formatDataForPreview(data[key], false)}`; formatted += `${key.toString()}: ${formatDataForPreview(
data[key],
false,
)}`;
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
// Prevent doing a lot of unnecessary iteration... // Prevent doing a lot of unnecessary iteration...
break; break;

View File

@ -17,6 +17,7 @@ import CustomObject from './CustomObject';
import EdgeCaseObjects from './EdgeCaseObjects.js'; import EdgeCaseObjects from './EdgeCaseObjects.js';
import NestedProps from './NestedProps'; import NestedProps from './NestedProps';
import SimpleValues from './SimpleValues'; import SimpleValues from './SimpleValues';
import SymbolKeys from './SymbolKeys';
// TODO Add Immutable JS example // TODO Add Immutable JS example
@ -32,6 +33,7 @@ export default function InspectableElements() {
<CustomObject /> <CustomObject />
<EdgeCaseObjects /> <EdgeCaseObjects />
<CircularReferences /> <CircularReferences />
<SymbolKeys />
</Fragment> </Fragment>
); );
} }

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
const base = Object.create(Object.prototype, {
enumerableStringBase: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('enumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableStringBase: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('nonEnumerableSymbolBase')]: {
value: 1,
writable: true,
enumerable: false,
configurable: true,
},
});
const data = Object.create(base, {
enumerableString: {
value: 2,
writable: true,
enumerable: true,
configurable: true,
},
nonEnumerableString: {
value: 3,
writable: true,
enumerable: false,
configurable: true,
},
[123]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
[Symbol('nonEnumerableSymbol')]: {
value: 2,
writable: true,
enumerable: false,
configurable: true,
},
[Symbol('enumerableSymbol')]: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
},
});
export default function SymbolKeys() {
return <ChildComponent data={data} />;
}
function ChildComponent(props: any) {
return null;
}