React DevTools: Show symbols used as keys in state (#19786)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
6h057 committed
Sep 14, 2020 at 15:55 UTC
917cb01a58f59725e3824f0b1912a23116c4cd7c
9 files changed
+415
-8
fixtures/devtools/standalone/index.html
+65
@@ -208,6 +208,70 @@
208
return <ChildComponent customObject={new Custom()} />;
209
}
210
211
+ const baseInheritedKeys = Object.create(Object.prototype, {
212
+ enumerableStringBase: {
213
+ value: 1,
214
+ writable: true,
215
+ enumerable: true,
216
+ configurable: true,
217
+ },
218
+ [Symbol('enumerableSymbolBase')]: {
219
+ value: 1,
220
+ writable: true,
221
+ enumerable: true,
222
+ configurable: true,
223
+ },
224
+ nonEnumerableStringBase: {
225
+ value: 1,
226
+ writable: true,
227
+ enumerable: false,
228
+ configurable: true,
229
+ },
230
+ [Symbol('nonEnumerableSymbolBase')]: {
231
+ value: 1,
232
+ writable: true,
233
+ enumerable: false,
234
+ configurable: true,
235
+ },
236
+ });
237
+
238
+ const inheritedKeys = Object.create(baseInheritedKeys, {
239
+ enumerableString: {
240
+ value: 2,
241
+ writable: true,
242
+ enumerable: true,
243
+ configurable: true,
244
+ },
245
+ nonEnumerableString: {
246
+ value: 3,
247
+ writable: true,
248
+ enumerable: false,
249
+ configurable: true,
250
+ },
251
+ 123: {
252
+ value: 3,
253
+ writable: true,
254
+ enumerable: true,
255
+ configurable: true,
256
+ },
257
+ [Symbol('nonEnumerableSymbol')]: {
258
+ value: 2,
259
+ writable: true,
260
+ enumerable: false,
261
+ configurable: true,
262
+ },
263
+ [Symbol('enumerableSymbol')]: {
264
+ value: 3,
265
+ writable: true,
266
+ enumerable: true,
267
+ configurable: true,
268
+ },
269
+ });
270
+
271
+ function InheritedKeys() {
272
+ return <ChildComponent data={inheritedKeys} />;
273
+ }
274
+
275
const object = {
276
string: "abc",
277
longString: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKJLMNOPQRSTUVWXYZ1234567890",
@@ -294,6 +358,7 @@
358
<ObjectProps />
359
<UnserializableProps />
360
<CustomObject />
361
+ <InheritedKeys />
362
</Fragment>
363
);
364
}
packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+22
@@ -541,6 +541,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
541
"object_of_objects": {
542
"inner": {}
543
},
544
+ "object_with_symbol": {
545
+ "Symbol(name)": "hello"
546
+ },
547
"proxy": {},
548
"react_element": {},
549
"regexp": {},
@@ -612,6 +615,25 @@ exports[`InspectedElementContext should support objects with overridden hasOwnPr
615
}
616
`;
617
618
+exports[`InspectedElementContext should support objects with with inherited keys: 1: Inspected element 2 1`] = `
619
+{
620
+ "id": 2,
621
+ "owners": null,
622
+ "context": null,
623
+ "hooks": null,
624
+ "props": {
625
+ "object": {
626
+ "123": 3,
627
+ "enumerableString": 2,
628
+ "Symbol(enumerableSymbol)": 3,
629
+ "enumerableStringBase": 1,
630
+ "Symbol(enumerableSymbolBase)": 1
631
+ }
632
+ },
633
+ "state": null
634
+}
635
+`;
636
+
637
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
638
{
639
"id": 2,
packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+112
@@ -537,6 +537,9 @@ describe('InspectedElementContext', () => {
537
const objectOfObjects = {
538
inner: {string: 'abc', number: 123, boolean: true},
539
};
540
+ const objectWithSymbol = {
541
+ [Symbol('name')]: 'hello',
542
+ };
543
const typedArray = Int8Array.from([100, -100, 0]);
544
const arrayBuffer = typedArray.buffer;
545
const dataView = new DataView(arrayBuffer);
@@ -580,6 +583,7 @@ describe('InspectedElementContext', () => {
583
map={mapShallow}
584
map_of_maps={mapOfMaps}
585
object_of_objects={objectOfObjects}
586
+ object_with_symbol={objectWithSymbol}
587
proxy={proxyInstance}
588
react_element={<span />}
589
regexp={/abc/giu}
@@ -633,6 +637,7 @@ describe('InspectedElementContext', () => {
637
map,
638
map_of_maps,
639
object_of_objects,
640
+ object_with_symbol,
641
proxy,
642
react_element,
643
regexp,
@@ -737,6 +742,8 @@ describe('InspectedElementContext', () => {
742
);
743
expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
744
745
+ expect(object_with_symbol['Symbol(name)']).toBe('hello');
746
+
747
expect(proxy[meta.inspectable]).toBe(false);
748
expect(proxy[meta.name]).toBe('function');
749
expect(proxy[meta.type]).toBe('function');
@@ -939,6 +946,111 @@ describe('InspectedElementContext', () => {
946
done();
947
});
948
949
+ it('should support objects with with inherited keys', async done => {
950
+ const Example = () => null;
951
+
952
+ const base = Object.create(Object.prototype, {
953
+ enumerableStringBase: {
954
+ value: 1,
955
+ writable: true,
956
+ enumerable: true,
957
+ configurable: true,
958
+ },
959
+ [Symbol('enumerableSymbolBase')]: {
960
+ value: 1,
961
+ writable: true,
962
+ enumerable: true,
963
+ configurable: true,
964
+ },
965
+ nonEnumerableStringBase: {
966
+ value: 1,
967
+ writable: true,
968
+ enumerable: false,
969
+ configurable: true,
970
+ },
971
+ [Symbol('nonEnumerableSymbolBase')]: {
972
+ value: 1,
973
+ writable: true,
974
+ enumerable: false,
975
+ configurable: true,
976
+ },
977
+ });
978
+
979
+ const object = Object.create(base, {
980
+ enumerableString: {
981
+ value: 2,
982
+ writable: true,
983
+ enumerable: true,
984
+ configurable: true,
985
+ },
986
+ nonEnumerableString: {
987
+ value: 3,
988
+ writable: true,
989
+ enumerable: false,
990
+ configurable: true,
991
+ },
992
+ [123]: {
993
+ value: 3,
994
+ writable: true,
995
+ enumerable: true,
996
+ configurable: true,
997
+ },
998
+ [Symbol('nonEnumerableSymbol')]: {
999
+ value: 2,
1000
+ writable: true,
1001
+ enumerable: false,
1002
+ configurable: true,
1003
+ },
1004
+ [Symbol('enumerableSymbol')]: {
1005
+ value: 3,
1006
+ writable: true,
1007
+ enumerable: true,
1008
+ configurable: true,
1009
+ },
1010
+ });
1011
+
1012
+ const container = document.createElement('div');
1013
+ await utils.actAsync(() =>
1014
+ ReactDOM.render(<Example object={object} />, container),
1015
+ );
1016
+
1017
+ const id = ((store.getElementIDAtIndex(0): any): number);
1018
+
1019
+ let inspectedElement = null;
1020
+
1021
+ function Suspender({target}) {
1022
+ const {getInspectedElement} = React.useContext(InspectedElementContext);
1023
+ inspectedElement = getInspectedElement(id);
1024
+ return null;
1025
+ }
1026
+
1027
+ await utils.actAsync(
1028
+ () =>
1029
+ TestRenderer.create(
1030
+ <Contexts
1031
+ defaultSelectedElementID={id}
1032
+ defaultSelectedElementIndex={0}>
1033
+ <React.Suspense fallback={null}>
1034
+ <Suspender target={id} />
1035
+ </React.Suspense>
1036
+ </Contexts>,
1037
+ ),
1038
+ false,
1039
+ );
1040
+
1041
+ expect(inspectedElement).not.toBeNull();
1042
+ expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
1043
+ expect(inspectedElement.props.object).toEqual({
1044
+ 123: 3,
1045
+ 'Symbol(enumerableSymbol)': 3,
1046
+ 'Symbol(enumerableSymbolBase)': 1,
1047
+ enumerableString: 2,
1048
+ enumerableStringBase: 1,
1049
+ });
1050
+
1051
+ done();
1052
+ });
1053
+
1054
it('should not dehydrate nested values until explicitly requested', async done => {
1055
const Example = () => {
1056
const [state] = React.useState({
packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+23
@@ -236,6 +236,29 @@ Object {
236
}
237
`;
238
239
+exports[`InspectedElementContext should support objects with with inherited keys: 1: Initial inspection 1`] = `
240
+Object {
241
+ "id": 2,
242
+ "type": "full-data",
243
+ "value": {
244
+ "id": 2,
245
+ "owners": null,
246
+ "context": {},
247
+ "hooks": null,
248
+ "props": {
249
+ "data": {
250
+ "123": 3,
251
+ "enumerableString": 2,
252
+ "Symbol(enumerableSymbol)": 3,
253
+ "enumerableStringBase": 1,
254
+ "Symbol(enumerableSymbolBase)": 1
255
+ }
256
+ },
257
+ "state": null
258
+},
259
+}
260
+`;
261
+
262
exports[`InspectedElementContext should support simple data types: 1: Initial inspection 1`] = `
263
Object {
264
"id": 2,
packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+75
@@ -432,6 +432,81 @@ describe('InspectedElementContext', () => {
432
done();
433
});
434
435
+ it('should support objects with with inherited keys', async done => {
436
+ const Example = () => null;
437
+
438
+ const base = Object.create(Object.prototype, {
439
+ enumerableStringBase: {
440
+ value: 1,
441
+ writable: true,
442
+ enumerable: true,
443
+ configurable: true,
444
+ },
445
+ [Symbol('enumerableSymbolBase')]: {
446
+ value: 1,
447
+ writable: true,
448
+ enumerable: true,
449
+ configurable: true,
450
+ },
451
+ nonEnumerableStringBase: {
452
+ value: 1,
453
+ writable: true,
454
+ enumerable: false,
455
+ configurable: true,
456
+ },
457
+ [Symbol('nonEnumerableSymbolBase')]: {
458
+ value: 1,
459
+ writable: true,
460
+ enumerable: false,
461
+ configurable: true,
462
+ },
463
+ });
464
+
465
+ const object = Object.create(base, {
466
+ enumerableString: {
467
+ value: 2,
468
+ writable: true,
469
+ enumerable: true,
470
+ configurable: true,
471
+ },
472
+ nonEnumerableString: {
473
+ value: 3,
474
+ writable: true,
475
+ enumerable: false,
476
+ configurable: true,
477
+ },
478
+ [123]: {
479
+ value: 3,
480
+ writable: true,
481
+ enumerable: true,
482
+ configurable: true,
483
+ },
484
+ [Symbol('nonEnumerableSymbol')]: {
485
+ value: 2,
486
+ writable: true,
487
+ enumerable: false,
488
+ configurable: true,
489
+ },
490
+ [Symbol('enumerableSymbol')]: {
491
+ value: 3,
492
+ writable: true,
493
+ enumerable: true,
494
+ configurable: true,
495
+ },
496
+ });
497
+
498
+ act(() =>
499
+ ReactDOM.render(<Example data={object} />, document.createElement('div')),
500
+ );
501
+
502
+ const id = ((store.getElementIDAtIndex(0): any): number);
503
+ const inspectedElement = await read(id);
504
+
505
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
506
+
507
+ done();
508
+ });
509
+
510
it('should not dehydrate nested values until explicitly requested', async done => {
511
const Example = () => null;
512
packages/react-devtools-shared/src/hydration.js
+5
-3
@@ -10,6 +10,7 @@
10
import {
11
getDataType,
12
getDisplayNameForReactElement,
13
+ getAllEnumerableKeys,
14
getInObject,
15
formatDataForPreview,
16
setInObject,
@@ -291,16 +292,17 @@ export function dehydrate(
292
return createDehydrated(type, true, data, cleaned, path);
293
} else {
294
const object = {};
294
- for (const name in data) {
295
+ getAllEnumerableKeys(data).forEach(key => {
296
+ const name = key.toString();
297
object[name] = dehydrate(
296
- data[name],
298
+ data[key],
299
cleaned,
300
unserializable,
301
path.concat([name]),
302
isPathAllowed,
303
isPathAllowedCheck ? 1 : level + 1,
304
);
303
- }
305
+ });
306
return object;
307
}
308
packages/react-devtools-shared/src/utils.js
+33
-5
@@ -52,16 +52,41 @@ const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
52
// Try to reuse the already encoded strings.
53
const encodedStringCache = new LRU({max: 1000});
54
55
-export function alphaSortKeys(a: string, b: string): number {
56
- if (a > b) {
55
+export function alphaSortKeys(
56
+ a: string | number | Symbol,
57
+ b: string | number | Symbol,
58
+): number {
59
+ if (a.toString() > b.toString()) {
60
return 1;
58
- } else if (b > a) {
61
+ } else if (b.toString() > a.toString()) {
62
return -1;
63
} else {
64
return 0;
65
}
66
}
67
68
+export function getAllEnumerableKeys(
69
+ obj: Object,
70
+): Array<string | number | Symbol> {
71
+ const keys = [];
72
+ let current = obj;
73
+ while (current != null) {
74
+ const currentKeys = [
75
+ ...Object.keys(current),
76
+ ...Object.getOwnPropertySymbols(current),
77
+ ];
78
+ const descriptors = Object.getOwnPropertyDescriptors(current);
79
+ currentKeys.forEach(key => {
80
+ // $FlowFixMe: key can be a Symbol https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
81
+ if (descriptors[key].enumerable) {
82
+ keys.push(key);
83
+ }
84
+ });
85
+ current = Object.getPrototypeOf(current);
86
+ }
87
+ return keys;
88
+}
89
+
90
export function getDisplayName(
91
type: Function,
92
fallbackName: string = 'Anonymous',
@@ -657,7 +682,7 @@ export function formatDataForPreview(
682
return data.toString();
683
case 'object':
684
if (showFormattedValue) {
660
- const keys = Object.keys(data).sort(alphaSortKeys);
685
+ const keys = getAllEnumerableKeys(data).sort(alphaSortKeys);
686
687
let formatted = '';
688
for (let i = 0; i < keys.length; i++) {
@@ -665,7 +690,10 @@ export function formatDataForPreview(
690
if (i > 0) {
691
formatted += ', ';
692
}
668
- formatted += `${key}: ${formatDataForPreview(data[key], false)}`;
693
+ formatted += `${key.toString()}: ${formatDataForPreview(
694
+ data[key],
695
+ false,
696
+ )}`;
697
if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
698
// Prevent doing a lot of unnecessary iteration...
699
break;
packages/react-devtools-shell/src/app/InspectableElements/InspectableElements.js
+2
@@ -17,6 +17,7 @@ import CustomObject from './CustomObject';
17
import EdgeCaseObjects from './EdgeCaseObjects.js';
18
import NestedProps from './NestedProps';
19
import SimpleValues from './SimpleValues';
20
+import SymbolKeys from './SymbolKeys';
21
22
// TODO Add Immutable JS example
23
@@ -32,6 +33,7 @@ export default function InspectableElements() {
33
<CustomObject />
34
<EdgeCaseObjects />
35
<CircularReferences />
36
+ <SymbolKeys />
37
</Fragment>
38
);
39
}
packages/react-devtools-shell/src/app/InspectableElements/SymbolKeys.js
new
+78
@@ -0,0 +1,78 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import * as React from 'react';
11
+
12
+const base = Object.create(Object.prototype, {
13
+ enumerableStringBase: {
14
+ value: 1,
15
+ writable: true,
16
+ enumerable: true,
17
+ configurable: true,
18
+ },
19
+ [Symbol('enumerableSymbolBase')]: {
20
+ value: 1,
21
+ writable: true,
22
+ enumerable: true,
23
+ configurable: true,
24
+ },
25
+ nonEnumerableStringBase: {
26
+ value: 1,
27
+ writable: true,
28
+ enumerable: false,
29
+ configurable: true,
30
+ },
31
+ [Symbol('nonEnumerableSymbolBase')]: {
32
+ value: 1,
33
+ writable: true,
34
+ enumerable: false,
35
+ configurable: true,
36
+ },
37
+});
38
+
39
+const data = Object.create(base, {
40
+ enumerableString: {
41
+ value: 2,
42
+ writable: true,
43
+ enumerable: true,
44
+ configurable: true,
45
+ },
46
+ nonEnumerableString: {
47
+ value: 3,
48
+ writable: true,
49
+ enumerable: false,
50
+ configurable: true,
51
+ },
52
+ [123]: {
53
+ value: 3,
54
+ writable: true,
55
+ enumerable: true,
56
+ configurable: true,
57
+ },
58
+ [Symbol('nonEnumerableSymbol')]: {
59
+ value: 2,
60
+ writable: true,
61
+ enumerable: false,
62
+ configurable: true,
63
+ },
64
+ [Symbol('enumerableSymbol')]: {
65
+ value: 3,
66
+ writable: true,
67
+ enumerable: true,
68
+ configurable: true,
69
+ },
70
+});
71
+
72
+export default function SymbolKeys() {
73
+ return <ChildComponent data={data} />;
74
+}
75
+
76
+function ChildComponent(props: any) {
77
+ return null;
78
+}