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()} />;
}
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 = {
string: "abc",
longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
@ -294,6 +358,7 @@
<ObjectProps />
<UnserializableProps />
<CustomObject />
<InheritedKeys />
</Fragment>
);
}

View File

@ -541,6 +541,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
"object_of_objects": {
"inner": {}
},
"object_with_symbol": {
"Symbol(name)": "hello"
},
"proxy": {},
"react_element": {},
"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`] = `
{
"id": 2,

View File

@ -537,6 +537,9 @@ describe('InspectedElementContext', () => {
const objectOfObjects = {
inner: {string: 'abc', number: 123, boolean: true},
};
const objectWithSymbol = {
[Symbol('name')]: 'hello',
};
const typedArray = Int8Array.from([100, -100, 0]);
const arrayBuffer = typedArray.buffer;
const dataView = new DataView(arrayBuffer);
@ -580,6 +583,7 @@ describe('InspectedElementContext', () => {
map={mapShallow}
map_of_maps={mapOfMaps}
object_of_objects={objectOfObjects}
object_with_symbol={objectWithSymbol}
proxy={proxyInstance}
react_element={<span />}
regexp={/abc/giu}
@ -633,6 +637,7 @@ describe('InspectedElementContext', () => {
map,
map_of_maps,
object_of_objects,
object_with_symbol,
proxy,
react_element,
regexp,
@ -737,6 +742,8 @@ describe('InspectedElementContext', () => {
);
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.name]).toBe('function');
expect(proxy[meta.type]).toBe('function');
@ -939,6 +946,111 @@ describe('InspectedElementContext', () => {
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 => {
const Example = () => {
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`] = `
Object {
"id": 2,

View File

@ -432,6 +432,81 @@ describe('InspectedElementContext', () => {
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 => {
const Example = () => null;

View File

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

View File

@ -52,16 +52,41 @@ const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
// Try to reuse the already encoded strings.
const encodedStringCache = new LRU({max: 1000});
export function alphaSortKeys(a: string, b: string): number {
if (a > b) {
export function alphaSortKeys(
a: string | number | Symbol,
b: string | number | Symbol,
): number {
if (a.toString() > b.toString()) {
return 1;
} else if (b > a) {
} else if (b.toString() > a.toString()) {
return -1;
} else {
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(
type: Function,
fallbackName: string = 'Anonymous',
@ -657,7 +682,7 @@ export function formatDataForPreview(
return data.toString();
case 'object':
if (showFormattedValue) {
const keys = Object.keys(data).sort(alphaSortKeys);
const keys = getAllEnumerableKeys(data).sort(alphaSortKeys);
let formatted = '';
for (let i = 0; i < keys.length; i++) {
@ -665,7 +690,10 @@ export function formatDataForPreview(
if (i > 0) {
formatted += ', ';
}
formatted += `${key}: ${formatDataForPreview(data[key], false)}`;
formatted += `${key.toString()}: ${formatDataForPreview(
data[key],
false,
)}`;
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
// Prevent doing a lot of unnecessary iteration...
break;

View File

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