@samitouri / QOS-React-2 / commits / 917cb01a58

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 +}