Initial support for hydration added to both renderer interfaces.
Brian Vaughn committed
Jun 16, 2019 at 17:32 UTC
a6d3f30f95f7d5d3a5c637f878b2b2cdb3fbe65b
19 files changed
+1564
-239
shells/dev/app/InspectableElements/Hydration.js
new
+54
@@ -0,0 +1,54 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+const div = document.createElement('div');
6
+const exmapleFunction = () => {};
7
+const typedArray = new Uint8Array(3);
8
+typedArray[0] = 1;
9
+typedArray[1] = 2;
10
+typedArray[2] = 3;
11
+
12
+const arrayOfArrays = [
13
+ [['a', 'b', 'c'], ['d', 'e', 'f'], ['h', 'i', 'j']],
14
+ [['k', 'l', 'm'], ['n', 'o', 'p'], ['q', 'r', 's']],
15
+ [['t', 'u', 'v'], ['w', 'x', 'y'], ['z']],
16
+];
17
+
18
+const objectOfObjects = {
19
+ foo: {
20
+ a: 1,
21
+ b: 2,
22
+ c: 3,
23
+ },
24
+ bar: {
25
+ e: 4,
26
+ f: 5,
27
+ g: 6,
28
+ },
29
+ baz: {
30
+ h: 7,
31
+ i: 8,
32
+ j: 9,
33
+ },
34
+};
35
+
36
+export default function Hydration() {
37
+ return (
38
+ <ChildComponent
39
+ html_element={div}
40
+ fn={exmapleFunction}
41
+ symbol={Symbol('symbol')}
42
+ react_element={<span />}
43
+ array_buffer={typedArray.buffer}
44
+ typed_array={typedArray}
45
+ date={new Date()}
46
+ array={arrayOfArrays}
47
+ object={objectOfObjects}
48
+ />
49
+ );
50
+}
51
+
52
+function ChildComponent(props: any) {
53
+ return null;
54
+}
shells/dev/app/InspectableElements/InspectableElements.js
+2
@@ -4,6 +4,7 @@ import React, { Fragment } from 'react';
4
import Contexts from './Contexts';
5
import CustomHooks from './CustomHooks';
6
import CustomObject from './CustomObject';
7
+import Hydration from './Hydration';
8
import NestedProps from './NestedProps';
9
10
// TODO Add Immutable JS example
@@ -16,6 +17,7 @@ export default function InspectableElements() {
17
<Contexts />
18
<CustomHooks />
19
<CustomObject />
20
+ <Hydration />
21
</Fragment>
22
);
23
}
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+248
-31
@@ -1,11 +1,101 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
-exports[`InspectedElementContext should inspect the currently selected element: 1: mount 1`] = `
4
-[root]
5
- <Example>
3
+exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
4
+{
5
+ "id": 2,
6
+ "owners": null,
7
+ "context": null,
8
+ "events": null,
9
+ "hooks": null,
10
+ "props": {
11
+ "nestedObject": {
12
+ "a": {},
13
+ "c": {}
14
+ }
15
+ },
16
+ "state": null
17
+}
18
+`;
19
+
20
+exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 2: Inspect props.nestedObject.a 1`] = `
21
+{
22
+ "id": 2,
23
+ "owners": null,
24
+ "context": null,
25
+ "events": null,
26
+ "hooks": null,
27
+ "props": {
28
+ "nestedObject": {
29
+ "a": {
30
+ "value": 1,
31
+ "b": {
32
+ "value": 1
33
+ }
34
+ },
35
+ "c": {}
36
+ }
37
+ },
38
+ "state": null
39
+}
40
`;
41
8
-exports[`InspectedElementContext should inspect the currently selected element: 2: Inspected element 2 1`] = `
42
+exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 3: Inspect props.nestedObject.c 1`] = `
43
+{
44
+ "id": 2,
45
+ "owners": null,
46
+ "context": null,
47
+ "events": null,
48
+ "hooks": null,
49
+ "props": {
50
+ "nestedObject": {
51
+ "a": {
52
+ "value": 1,
53
+ "b": {
54
+ "value": 1
55
+ }
56
+ },
57
+ "c": {
58
+ "value": 1,
59
+ "d": {
60
+ "value": 1,
61
+ "e": {}
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "state": null
67
+}
68
+`;
69
+
70
+exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 4: update inspected element 1`] = `
71
+{
72
+ "id": 2,
73
+ "owners": null,
74
+ "context": null,
75
+ "events": null,
76
+ "hooks": null,
77
+ "props": {
78
+ "nestedObject": {
79
+ "a": {
80
+ "value": 2,
81
+ "b": {
82
+ "value": 2
83
+ }
84
+ },
85
+ "c": {
86
+ "value": 2,
87
+ "d": {
88
+ "value": 2,
89
+ "e": {}
90
+ }
91
+ }
92
+ }
93
+ },
94
+ "state": null
95
+}
96
+`;
97
+
98
+exports[`InspectedElementContext should inspect the currently selected element: 1: Inspected element 2 1`] = `
99
{
100
"id": 2,
101
"owners": null,
@@ -17,24 +107,104 @@ exports[`InspectedElementContext should inspect the currently selected element:
107
"isStateEditable": true,
108
"name": "State",
109
"value": 1,
20
- "subHooks": []
110
+ "subHooks": {}
111
}
112
],
113
"props": {
24
- "foo": 1,
25
- "bar": "abc"
114
+ "a": 1,
115
+ "b": "abc"
116
+ },
117
+ "state": null
118
+}
119
+`;
120
+
121
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
122
+{
123
+ "id": 2,
124
+ "owners": null,
125
+ "context": null,
126
+ "events": null,
127
+ "hooks": null,
128
+ "props": {
129
+ "nestedObject": {
130
+ "a": {}
131
+ }
132
+ },
133
+ "state": null
134
+}
135
+`;
136
+
137
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
138
+{
139
+ "id": 2,
140
+ "owners": null,
141
+ "context": null,
142
+ "events": null,
143
+ "hooks": null,
144
+ "props": {
145
+ "nestedObject": {
146
+ "a": {
147
+ "b": {
148
+ "c": {}
149
+ }
150
+ }
151
+ }
152
+ },
153
+ "state": null
154
+}
155
+`;
156
+
157
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
158
+{
159
+ "id": 2,
160
+ "owners": null,
161
+ "context": null,
162
+ "events": null,
163
+ "hooks": null,
164
+ "props": {
165
+ "nestedObject": {
166
+ "a": {
167
+ "b": {
168
+ "c": [
169
+ {
170
+ "d": {}
171
+ }
172
+ ]
173
+ }
174
+ }
175
+ }
176
},
177
"state": null
178
}
179
`;
180
31
-exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: mount 1`] = `
32
-[root]
33
- ▾ <Wrapper>
34
- <Anonymous>
181
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
182
+{
183
+ "id": 2,
184
+ "owners": null,
185
+ "context": null,
186
+ "events": null,
187
+ "hooks": null,
188
+ "props": {
189
+ "nestedObject": {
190
+ "a": {
191
+ "b": {
192
+ "c": [
193
+ {
194
+ "d": {
195
+ "e": {}
196
+ }
197
+ }
198
+ ]
199
+ }
200
+ }
201
+ }
202
+ },
203
+ "state": null
204
+}
205
`;
206
37
-exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: initial render 1`] = `
207
+exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: initial render 1`] = `
208
{
209
"id": 3,
210
"owners": null,
@@ -46,18 +216,18 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
216
"isStateEditable": true,
217
"name": "State",
218
"value": 0,
49
- "subHooks": []
219
+ "subHooks": {}
220
}
221
],
222
"props": {
53
- "foo": 1,
54
- "bar": "abc"
223
+ "a": 1,
224
+ "b": "abc"
225
},
226
"state": null
227
}
228
`;
229
60
-exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 3: updated state 1`] = `
230
+exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: updated state 1`] = `
231
{
232
"id": 3,
233
"owners": null,
@@ -69,23 +239,34 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
239
"isStateEditable": true,
240
"name": "State",
241
"value": 0,
72
- "subHooks": []
242
+ "subHooks": {}
243
}
244
],
245
"props": {
76
- "foo": 2,
77
- "bar": "def"
246
+ "a": 2,
247
+ "b": "def"
248
},
249
"state": null
250
}
251
`;
252
83
-exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = `
84
-[root]
85
- <Example>
253
+exports[`InspectedElementContext should not tear if hydration is requested after an update: 1: Initially inspect element 1`] = `
254
+{
255
+ "id": 2,
256
+ "owners": null,
257
+ "context": null,
258
+ "events": null,
259
+ "hooks": null,
260
+ "props": {
261
+ "nestedObject": {
262
+ "a": {}
263
+ }
264
+ },
265
+ "state": null
266
+}
267
`;
268
88
-exports[`InspectedElementContext should poll for updates for the currently selected element: 2: initial render 1`] = `
269
+exports[`InspectedElementContext should not tear if hydration is requested after an update: 2: Inspect props.nestedObject.a 1`] = `
270
{
271
"id": 2,
272
"owners": null,
@@ -93,8 +274,29 @@ exports[`InspectedElementContext should poll for updates for the currently selec
274
"events": null,
275
"hooks": null,
276
"props": {
96
- "foo": 1,
97
- "bar": "abc"
277
+ "nestedObject": {
278
+ "a": {
279
+ "value": 2,
280
+ "b": {
281
+ "value": 2
282
+ }
283
+ }
284
+ }
285
+ },
286
+ "state": null
287
+}
288
+`;
289
+
290
+exports[`InspectedElementContext should poll for updates for the currently selected element: 1: initial render 1`] = `
291
+{
292
+ "id": 2,
293
+ "owners": null,
294
+ "context": null,
295
+ "events": null,
296
+ "hooks": null,
297
+ "props": {
298
+ "a": 1,
299
+ "b": "abc"
300
},
301
"state": null
302
}
@@ -108,19 +310,34 @@ exports[`InspectedElementContext should poll for updates for the currently selec
310
"events": null,
311
"hooks": null,
312
"props": {
111
- "foo": 2,
112
- "bar": "def"
313
+ "a": 2,
314
+ "b": "def"
315
},
316
"state": null
317
}
318
`;
319
118
-exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: mount 1`] = `
119
-[root]
120
- <Example>
320
+exports[`InspectedElementContext should support complex data types: 1: Inspected element 2 1`] = `
321
+{
322
+ "id": 2,
323
+ "owners": null,
324
+ "context": null,
325
+ "events": null,
326
+ "hooks": null,
327
+ "props": {
328
+ "html_element": {},
329
+ "fn": {},
330
+ "symbol": {},
331
+ "react_element": {},
332
+ "array_buffer": {},
333
+ "typed_array": {},
334
+ "date": {}
335
+ },
336
+ "state": null
337
+}
338
`;
339
123
-exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 2: Inspected element 2 1`] = `
340
+exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Inspected element 2 1`] = `
341
{
342
"id": 2,
343
"owners": null,
src/__tests__/inspectedElementContext-test.js
+383
-37
@@ -1,7 +1,7 @@
1
// @flow
2
3
import typeof ReactTestRenderer from 'react-test-renderer';
4
-import type { Element } from 'src/devtools/views/Components/types';
4
+import type { GetPath } from 'src/devtools/views/Components/InspectedElementContext';
5
import type Bridge from 'src/bridge';
6
import type Store from 'src/devtools/store';
7
@@ -11,24 +11,29 @@ describe('InspectedElementContext', () => {
11
let TestRenderer: ReactTestRenderer;
12
let bridge: Bridge;
13
let store: Store;
14
+ let meta;
15
let utils;
16
17
let BridgeContext;
18
let InspectedElementContext;
19
let InspectedElementContextController;
20
let StoreContext;
21
+ let TestUtils;
22
let TreeContextController;
23
24
beforeEach(() => {
25
utils = require('./utils');
26
utils.beforeEachProfiling();
27
28
+ meta = require('src/hydration').meta;
29
+
30
bridge = global.bridge;
31
store = global.store;
32
store.collapseNodesByDefault = false;
33
34
React = require('react');
35
ReactDOM = require('react-dom');
36
+ TestUtils = require('react-dom/test-utils');
37
TestRenderer = utils.requireTestRenderer();
38
39
BridgeContext = require('src/devtools/views/context').BridgeContext;
@@ -68,20 +73,17 @@ describe('InspectedElementContext', () => {
73
74
const container = document.createElement('div');
75
await utils.actAsync(() =>
71
- ReactDOM.render(<Example foo={1} bar="abc" />, container)
76
+ ReactDOM.render(<Example a={1} b="abc" />, container)
77
);
73
- expect(store).toMatchSnapshot('1: mount');
78
75
- const example = ((store.getElementAtIndex(0): any): Element);
79
+ const id = ((store.getElementIDAtIndex(0): any): number);
80
81
let didFinish = false;
82
83
function Suspender({ target }) {
84
const { read } = React.useContext(InspectedElementContext);
81
- const inspectedElement = read(target.id);
82
- expect(inspectedElement).toMatchSnapshot(
83
- `2: Inspected element ${target.id}`
84
- );
85
+ const inspectedElement = read(id);
86
+ expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
87
didFinish = true;
88
return null;
89
}
@@ -90,11 +92,11 @@ describe('InspectedElementContext', () => {
92
() =>
93
TestRenderer.create(
94
<Contexts
93
- defaultSelectedElementID={example.id}
95
+ defaultSelectedElementID={id}
96
defaultSelectedElementIndex={0}
97
>
98
<React.Suspense fallback={null}>
97
- <Suspender target={example} />
99
+ <Suspender target={id} />
100
</React.Suspense>
101
</Contexts>
102
),
@@ -110,18 +112,17 @@ describe('InspectedElementContext', () => {
112
113
const container = document.createElement('div');
114
await utils.actAsync(
113
- () => ReactDOM.render(<Example foo={1} bar="abc" />, container),
115
+ () => ReactDOM.render(<Example a={1} b="abc" />, container),
116
false
117
);
116
- expect(store).toMatchSnapshot('1: mount');
118
118
- const example = ((store.getElementAtIndex(0): any): Element);
119
+ const id = ((store.getElementIDAtIndex(0): any): number);
120
121
let inspectedElement = null;
122
123
function Suspender({ target }) {
124
const { read } = React.useContext(InspectedElementContext);
124
- inspectedElement = read(target.id);
125
+ inspectedElement = read(id);
126
return null;
127
}
128
@@ -129,20 +130,17 @@ describe('InspectedElementContext', () => {
130
131
await utils.actAsync(() => {
132
renderer = TestRenderer.create(
132
- <Contexts
133
- defaultSelectedElementID={example.id}
134
- defaultSelectedElementIndex={0}
135
- >
133
+ <Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={0}>
134
<React.Suspense fallback={null}>
137
- <Suspender target={example} />
135
+ <Suspender target={id} />
136
</React.Suspense>
137
</Contexts>
138
);
139
}, false);
142
- expect(inspectedElement).toMatchSnapshot('2: initial render');
140
+ expect(inspectedElement).toMatchSnapshot('1: initial render');
141
142
await utils.actAsync(
145
- () => ReactDOM.render(<Example foo={2} bar="def" />, container),
143
+ () => ReactDOM.render(<Example a={2} b="def" />, container),
144
false
145
);
146
@@ -151,11 +149,11 @@ describe('InspectedElementContext', () => {
149
() =>
150
renderer.update(
151
<Contexts
154
- defaultSelectedElementID={example.id}
152
+ defaultSelectedElementID={id}
153
defaultSelectedElementIndex={0}
154
>
155
<React.Suspense fallback={null}>
158
- <Suspender target={example} />
156
+ <Suspender target={id} />
157
</React.Suspense>
158
</Contexts>
159
),
@@ -180,12 +178,11 @@ describe('InspectedElementContext', () => {
178
await utils.actAsync(() =>
179
ReactDOM.render(
180
<Wrapper>
183
- <Target foo={1} bar="abc" />
181
+ <Target a={1} b="abc" />
182
</Wrapper>,
183
container
184
)
185
);
188
- expect(store).toMatchSnapshot('1: mount');
186
187
const id = ((store.getElementIDAtIndex(1): any): number);
188
@@ -215,7 +212,7 @@ describe('InspectedElementContext', () => {
212
false
213
);
214
expect(targetRenderCount).toBe(1);
218
- expect(inspectedElement).toMatchSnapshot('2: initial render');
215
+ expect(inspectedElement).toMatchSnapshot('1: initial render');
216
217
const initialInspectedElement = inspectedElement;
218
@@ -244,7 +241,7 @@ describe('InspectedElementContext', () => {
241
() =>
242
ReactDOM.render(
243
<Wrapper>
247
- <Target foo={2} bar="def" />
244
+ <Target a={2} b="def" />
245
</Wrapper>,
246
container
247
),
@@ -253,7 +250,86 @@ describe('InspectedElementContext', () => {
250
251
// Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
252
expect(targetRenderCount).toBe(2);
256
- expect(inspectedElement).toMatchSnapshot('3: updated state');
253
+ expect(inspectedElement).toMatchSnapshot('2: updated state');
254
+
255
+ done();
256
+ });
257
+
258
+ it('should support complex data types', async done => {
259
+ const Example = () => null;
260
+
261
+ const div = document.createElement('div');
262
+ const exmapleFunction = () => {};
263
+ const typedArray = new Uint8Array(3);
264
+
265
+ const container = document.createElement('div');
266
+ await utils.actAsync(() =>
267
+ ReactDOM.render(
268
+ <Example
269
+ html_element={div}
270
+ fn={exmapleFunction}
271
+ symbol={Symbol('symbol')}
272
+ react_element={<span />}
273
+ array_buffer={typedArray.buffer}
274
+ typed_array={typedArray}
275
+ date={new Date()}
276
+ />,
277
+ container
278
+ )
279
+ );
280
+
281
+ const id = ((store.getElementIDAtIndex(0): any): number);
282
+
283
+ let inspectedElement = null;
284
+
285
+ function Suspender({ target }) {
286
+ const { read } = React.useContext(InspectedElementContext);
287
+ inspectedElement = read(id);
288
+ return null;
289
+ }
290
+
291
+ await utils.actAsync(
292
+ () =>
293
+ TestRenderer.create(
294
+ <Contexts
295
+ defaultSelectedElementID={id}
296
+ defaultSelectedElementIndex={0}
297
+ >
298
+ <React.Suspense fallback={null}>
299
+ <Suspender target={id} />
300
+ </React.Suspense>
301
+ </Contexts>
302
+ ),
303
+ false
304
+ );
305
+
306
+ expect(inspectedElement).not.toBeNull();
307
+ expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
308
+
309
+ const {
310
+ html_element,
311
+ fn,
312
+ symbol,
313
+ react_element,
314
+ array_buffer,
315
+ typed_array,
316
+ date,
317
+ } = (inspectedElement: any).props;
318
+ expect(html_element[meta.name]).toBe('DIV');
319
+ expect(html_element[meta.type]).toBe('html_element');
320
+ expect(fn[meta.name]).toBe('exmapleFunction');
321
+ expect(fn[meta.type]).toBe('function');
322
+ expect(symbol[meta.name]).toBe('Symbol(symbol)');
323
+ expect(symbol[meta.type]).toBe('symbol');
324
+ expect(react_element[meta.name]).toBe('span');
325
+ expect(react_element[meta.type]).toBe('react_element');
326
+ expect(array_buffer[meta.meta].length).toBe(3);
327
+ expect(array_buffer[meta.name]).toBe('ArrayBuffer');
328
+ expect(array_buffer[meta.type]).toBe('array_buffer');
329
+ expect(typed_array[meta.meta].length).toBe(3);
330
+ expect(typed_array[meta.name]).toBe('Uint8Array');
331
+ expect(typed_array[meta.type]).toBe('typed_array');
332
+ expect(date[meta.type]).toBe('date');
333
334
done();
335
});
@@ -276,24 +352,21 @@ describe('InspectedElementContext', () => {
352
descriptor.enumerable = true;
353
Object.defineProperty(CustomData.prototype, 'number', descriptor);
354
279
- const Example = ({ data }) => null;
355
+ const Example = () => null;
356
357
const container = document.createElement('div');
358
await utils.actAsync(() =>
359
ReactDOM.render(<Example data={new CustomData()} />, container)
360
);
285
- expect(store).toMatchSnapshot('1: mount');
361
287
- const example = ((store.getElementAtIndex(0): any): Element);
362
+ const id = ((store.getElementIDAtIndex(0): any): number);
363
364
let didFinish = false;
365
366
function Suspender({ target }) {
367
const { read } = React.useContext(InspectedElementContext);
293
- const inspectedElement = read(target.id);
294
- expect(inspectedElement).toMatchSnapshot(
295
- `2: Inspected element ${target.id}`
296
- );
368
+ const inspectedElement = read(id);
369
+ expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
370
didFinish = true;
371
return null;
372
}
@@ -302,11 +375,11 @@ describe('InspectedElementContext', () => {
375
() =>
376
TestRenderer.create(
377
<Contexts
305
- defaultSelectedElementID={example.id}
378
+ defaultSelectedElementID={id}
379
defaultSelectedElementIndex={0}
380
>
381
<React.Suspense fallback={null}>
309
- <Suspender target={example} />
382
+ <Suspender target={id} />
383
</React.Suspense>
384
</Contexts>
385
),
@@ -316,4 +389,277 @@ describe('InspectedElementContext', () => {
389
390
done();
391
});
392
+
393
+ it('should not dehydrate nested values until explicitly requested', async done => {
394
+ const Example = () => null;
395
+
396
+ const container = document.createElement('div');
397
+ await utils.actAsync(() =>
398
+ ReactDOM.render(
399
+ <Example
400
+ nestedObject={{
401
+ a: {
402
+ b: {
403
+ c: [
404
+ {
405
+ d: {
406
+ e: {},
407
+ },
408
+ },
409
+ ],
410
+ },
411
+ },
412
+ }}
413
+ />,
414
+ container
415
+ )
416
+ );
417
+
418
+ const id = ((store.getElementIDAtIndex(0): any): number);
419
+
420
+ let getPath: GetPath = ((null: any): GetPath);
421
+ let inspectedElement = null;
422
+
423
+ function Suspender({ target }) {
424
+ const context = React.useContext(InspectedElementContext);
425
+ getPath = context.getPath;
426
+ inspectedElement = context.read(target);
427
+ return null;
428
+ }
429
+
430
+ await utils.actAsync(
431
+ () =>
432
+ TestRenderer.create(
433
+ <Contexts
434
+ defaultSelectedElementID={id}
435
+ defaultSelectedElementIndex={0}
436
+ >
437
+ <React.Suspense fallback={null}>
438
+ <Suspender target={id} />
439
+ </React.Suspense>
440
+ </Contexts>
441
+ ),
442
+ false
443
+ );
444
+ expect(getPath).not.toBeNull();
445
+ expect(inspectedElement).not.toBeNull();
446
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
447
+
448
+ inspectedElement = null;
449
+ TestUtils.act(() => {
450
+ getPath(id, ['props', 'nestedObject', 'a']);
451
+ jest.runOnlyPendingTimers();
452
+ });
453
+ expect(inspectedElement).not.toBeNull();
454
+ expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
455
+
456
+ inspectedElement = null;
457
+ TestUtils.act(() => {
458
+ getPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
459
+ jest.runOnlyPendingTimers();
460
+ });
461
+ expect(inspectedElement).not.toBeNull();
462
+ expect(inspectedElement).toMatchSnapshot(
463
+ '3: Inspect props.nestedObject.a.b.c'
464
+ );
465
+
466
+ inspectedElement = null;
467
+ TestUtils.act(() => {
468
+ getPath(id, ['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
469
+ jest.runOnlyPendingTimers();
470
+ });
471
+ expect(inspectedElement).not.toBeNull();
472
+ expect(inspectedElement).toMatchSnapshot(
473
+ '4: Inspect props.nestedObject.a.b.c.0.d'
474
+ );
475
+
476
+ done();
477
+ });
478
+
479
+ it('should include updates for nested values that were previously hydrated', async done => {
480
+ const Example = () => null;
481
+
482
+ const container = document.createElement('div');
483
+ await utils.actAsync(() =>
484
+ ReactDOM.render(
485
+ <Example
486
+ nestedObject={{
487
+ a: {
488
+ value: 1,
489
+ b: {
490
+ value: 1,
491
+ },
492
+ },
493
+ c: {
494
+ value: 1,
495
+ d: {
496
+ value: 1,
497
+ e: {
498
+ value: 1,
499
+ },
500
+ },
501
+ },
502
+ }}
503
+ />,
504
+ container
505
+ )
506
+ );
507
+
508
+ const id = ((store.getElementIDAtIndex(0): any): number);
509
+
510
+ let getPath: GetPath = ((null: any): GetPath);
511
+ let inspectedElement = null;
512
+
513
+ function Suspender({ target }) {
514
+ const context = React.useContext(InspectedElementContext);
515
+ getPath = context.getPath;
516
+ inspectedElement = context.read(id);
517
+ return null;
518
+ }
519
+
520
+ await utils.actAsync(
521
+ () =>
522
+ TestRenderer.create(
523
+ <Contexts
524
+ defaultSelectedElementID={id}
525
+ defaultSelectedElementIndex={0}
526
+ >
527
+ <React.Suspense fallback={null}>
528
+ <Suspender target={id} />
529
+ </React.Suspense>
530
+ </Contexts>
531
+ ),
532
+ false
533
+ );
534
+ expect(getPath).not.toBeNull();
535
+ expect(inspectedElement).not.toBeNull();
536
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
537
+
538
+ inspectedElement = null;
539
+ TestUtils.act(() => {
540
+ getPath(id, ['props', 'nestedObject', 'a']);
541
+ jest.runOnlyPendingTimers();
542
+ });
543
+ expect(inspectedElement).not.toBeNull();
544
+ expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
545
+
546
+ inspectedElement = null;
547
+ TestUtils.act(() => {
548
+ getPath(id, ['props', 'nestedObject', 'c']);
549
+ jest.runOnlyPendingTimers();
550
+ });
551
+ expect(inspectedElement).not.toBeNull();
552
+ expect(inspectedElement).toMatchSnapshot('3: Inspect props.nestedObject.c');
553
+
554
+ TestUtils.act(() => {
555
+ ReactDOM.render(
556
+ <Example
557
+ nestedObject={{
558
+ a: {
559
+ value: 2,
560
+ b: {
561
+ value: 2,
562
+ },
563
+ },
564
+ c: {
565
+ value: 2,
566
+ d: {
567
+ value: 2,
568
+ e: {
569
+ value: 2,
570
+ },
571
+ },
572
+ },
573
+ }}
574
+ />,
575
+ container
576
+ );
577
+ });
578
+
579
+ TestUtils.act(() => {
580
+ inspectedElement = null;
581
+ jest.runOnlyPendingTimers();
582
+ expect(inspectedElement).not.toBeNull();
583
+ expect(inspectedElement).toMatchSnapshot('4: update inspected element');
584
+ });
585
+
586
+ done();
587
+ });
588
+
589
+ it('should not tear if hydration is requested after an update', async done => {
590
+ const Example = () => null;
591
+
592
+ const container = document.createElement('div');
593
+ await utils.actAsync(() =>
594
+ ReactDOM.render(
595
+ <Example
596
+ nestedObject={{
597
+ a: {
598
+ value: 1,
599
+ b: {
600
+ value: 1,
601
+ },
602
+ },
603
+ }}
604
+ />,
605
+ container
606
+ )
607
+ );
608
+
609
+ const id = ((store.getElementIDAtIndex(0): any): number);
610
+
611
+ let getPath: GetPath = ((null: any): GetPath);
612
+ let inspectedElement = null;
613
+
614
+ function Suspender({ target }) {
615
+ const context = React.useContext(InspectedElementContext);
616
+ getPath = context.getPath;
617
+ inspectedElement = context.read(id);
618
+ return null;
619
+ }
620
+
621
+ await utils.actAsync(
622
+ () =>
623
+ TestRenderer.create(
624
+ <Contexts
625
+ defaultSelectedElementID={id}
626
+ defaultSelectedElementIndex={0}
627
+ >
628
+ <React.Suspense fallback={null}>
629
+ <Suspender target={id} />
630
+ </React.Suspense>
631
+ </Contexts>
632
+ ),
633
+ false
634
+ );
635
+ expect(getPath).not.toBeNull();
636
+ expect(inspectedElement).not.toBeNull();
637
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
638
+
639
+ TestUtils.act(() => {
640
+ ReactDOM.render(
641
+ <Example
642
+ nestedObject={{
643
+ a: {
644
+ value: 2,
645
+ b: {
646
+ value: 2,
647
+ },
648
+ },
649
+ }}
650
+ />,
651
+ container
652
+ );
653
+ });
654
+
655
+ inspectedElement = null;
656
+ TestUtils.act(() => {
657
+ getPath(id, ['props', 'nestedObject', 'a']);
658
+ jest.runOnlyPendingTimers();
659
+ });
660
+ expect(inspectedElement).not.toBeNull();
661
+ expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
662
+
663
+ done();
664
+ });
665
});
src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
new
+167
@@ -0,0 +1,167 @@
1
+// Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+exports[`InspectedElementContext should inspect the currently selected element: 1: Initial inspection 1`] = `
4
+Object {
5
+ "id": 2,
6
+ "type": "full-data",
7
+ "value": {
8
+ "id": 2,
9
+ "owners": null,
10
+ "context": {},
11
+ "events": null,
12
+ "hooks": null,
13
+ "props": {
14
+ "a": 1,
15
+ "b": "abc"
16
+ },
17
+ "state": null
18
+},
19
+}
20
+`;
21
+
22
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 1: Initially inspect element 1`] = `
23
+Object {
24
+ "id": 2,
25
+ "type": "full-data",
26
+ "value": {
27
+ "id": 2,
28
+ "owners": null,
29
+ "context": {},
30
+ "events": null,
31
+ "hooks": null,
32
+ "props": {
33
+ "nestedObject": {
34
+ "a": {}
35
+ }
36
+ },
37
+ "state": null
38
+},
39
+}
40
+`;
41
+
42
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 2: Inspect props.nestedObject.a 1`] = `
43
+Object {
44
+ "id": 2,
45
+ "type": "full-data",
46
+ "value": {
47
+ "id": 2,
48
+ "owners": null,
49
+ "context": {},
50
+ "events": null,
51
+ "hooks": null,
52
+ "props": {
53
+ "nestedObject": {
54
+ "a": {
55
+ "b": {
56
+ "c": {}
57
+ }
58
+ }
59
+ }
60
+ },
61
+ "state": null
62
+},
63
+}
64
+`;
65
+
66
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 3: Inspect props.nestedObject.a.b.c 1`] = `
67
+Object {
68
+ "id": 2,
69
+ "type": "full-data",
70
+ "value": {
71
+ "id": 2,
72
+ "owners": null,
73
+ "context": {},
74
+ "events": null,
75
+ "hooks": null,
76
+ "props": {
77
+ "nestedObject": {
78
+ "a": {
79
+ "b": {
80
+ "c": [
81
+ {
82
+ "d": {}
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ }
88
+ },
89
+ "state": null
90
+},
91
+}
92
+`;
93
+
94
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 4: Inspect props.nestedObject.a.b.c.0.d 1`] = `
95
+Object {
96
+ "id": 2,
97
+ "type": "full-data",
98
+ "value": {
99
+ "id": 2,
100
+ "owners": null,
101
+ "context": {},
102
+ "events": null,
103
+ "hooks": null,
104
+ "props": {
105
+ "nestedObject": {
106
+ "a": {
107
+ "b": {
108
+ "c": [
109
+ {
110
+ "d": {
111
+ "e": {}
112
+ }
113
+ }
114
+ ]
115
+ }
116
+ }
117
+ }
118
+ },
119
+ "state": null
120
+},
121
+}
122
+`;
123
+
124
+exports[`InspectedElementContext should support complex data types: 1: Initial inspection 1`] = `
125
+Object {
126
+ "id": 2,
127
+ "type": "full-data",
128
+ "value": {
129
+ "id": 2,
130
+ "owners": null,
131
+ "context": {},
132
+ "events": null,
133
+ "hooks": null,
134
+ "props": {
135
+ "html_element": {},
136
+ "fn": {},
137
+ "symbol": {},
138
+ "react_element": {},
139
+ "array_buffer": {},
140
+ "typed_array": {},
141
+ "date": {}
142
+ },
143
+ "state": null
144
+},
145
+}
146
+`;
147
+
148
+exports[`InspectedElementContext should support custom objects with enumerable properties and getters: 1: Initial inspection 1`] = `
149
+Object {
150
+ "id": 2,
151
+ "type": "full-data",
152
+ "value": {
153
+ "id": 2,
154
+ "owners": null,
155
+ "context": {},
156
+ "events": null,
157
+ "hooks": null,
158
+ "props": {
159
+ "data": {
160
+ "_number": 42,
161
+ "number": 42
162
+ }
163
+ },
164
+ "state": null
165
+},
166
+}
167
+`;
src/__tests__/legacy/inspectElement-test.js
new
+233
@@ -0,0 +1,233 @@
1
+// @flow
2
+
3
+import type { InspectedElementPayload } from 'src/backend/types';
4
+import type { DehydratedData } from 'src/devtools/views/Components/types';
5
+import type Bridge from 'src/bridge';
6
+import type Store from 'src/devtools/store';
7
+
8
+describe('InspectedElementContext', () => {
9
+ let React;
10
+ let ReactDOM;
11
+ let hydrate;
12
+ let meta;
13
+ let bridge: Bridge;
14
+ let store: Store;
15
+
16
+ const act = (callback: Function) => {
17
+ callback();
18
+
19
+ jest.runAllTimers(); // Flush Bridge operations
20
+ };
21
+
22
+ function dehydrateHelper(
23
+ dehydratedData: DehydratedData | null
24
+ ): Object | null {
25
+ if (dehydratedData !== null) {
26
+ return hydrate(dehydratedData.data, dehydratedData.cleaned);
27
+ } else {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ async function read(
33
+ id: number,
34
+ path?: Array<string | number>
35
+ ): Promise<Object> {
36
+ return new Promise((resolve, reject) => {
37
+ const rendererID = ((store.getRendererIDForElement(id): any): number);
38
+
39
+ const onInspectedElement = (payload: InspectedElementPayload) => {
40
+ bridge.removeListener('inspectedElement', onInspectedElement);
41
+
42
+ if (payload.type === 'full-data' && payload.value !== null) {
43
+ payload.value.context = dehydrateHelper(payload.value.context);
44
+ payload.value.props = dehydrateHelper(payload.value.props);
45
+ payload.value.state = dehydrateHelper(payload.value.state);
46
+ }
47
+
48
+ resolve(payload);
49
+ };
50
+
51
+ bridge.addListener('inspectedElement', onInspectedElement);
52
+ bridge.send('inspectElement', { id, path, rendererID });
53
+
54
+ jest.runOnlyPendingTimers();
55
+ });
56
+ }
57
+
58
+ beforeEach(() => {
59
+ bridge = global.bridge;
60
+ store = global.store;
61
+
62
+ hydrate = require('src/hydration').hydrate;
63
+ meta = require('src/hydration').meta;
64
+
65
+ // Redirect all React/ReactDOM requires to the v15 UMD.
66
+ // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
67
+ jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
68
+ jest.mock('react-dom', () =>
69
+ jest.requireActual('react-dom-15/dist/react-dom.js')
70
+ );
71
+
72
+ React = require('react');
73
+ ReactDOM = require('react-dom');
74
+ });
75
+
76
+ it('should inspect the currently selected element', async done => {
77
+ const Example = () => null;
78
+
79
+ act(() =>
80
+ ReactDOM.render(<Example a={1} b="abc" />, document.createElement('div'))
81
+ );
82
+
83
+ const id = ((store.getElementIDAtIndex(0): any): number);
84
+ const inspectedElement = await read(id);
85
+
86
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
87
+
88
+ done();
89
+ });
90
+
91
+ it('should support complex data types', async done => {
92
+ const Example = () => null;
93
+
94
+ const div = document.createElement('div');
95
+ const exmapleFunction = () => {};
96
+ const typedArray = new Uint8Array(3);
97
+
98
+ act(() =>
99
+ ReactDOM.render(
100
+ <Example
101
+ html_element={div}
102
+ fn={exmapleFunction}
103
+ symbol={Symbol('symbol')}
104
+ react_element={<span />}
105
+ array_buffer={typedArray.buffer}
106
+ typed_array={typedArray}
107
+ date={new Date()}
108
+ />,
109
+ document.createElement('div')
110
+ )
111
+ );
112
+
113
+ const id = ((store.getElementIDAtIndex(0): any): number);
114
+ const inspectedElement = await read(id);
115
+
116
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
117
+
118
+ const {
119
+ html_element,
120
+ fn,
121
+ symbol,
122
+ react_element,
123
+ array_buffer,
124
+ typed_array,
125
+ date,
126
+ } = inspectedElement.value.props;
127
+ expect(html_element[meta.name]).toBe('DIV');
128
+ expect(html_element[meta.type]).toBe('html_element');
129
+ expect(fn[meta.name]).toBe('exmapleFunction');
130
+ expect(fn[meta.type]).toBe('function');
131
+ expect(symbol[meta.name]).toBe('Symbol(symbol)');
132
+ expect(symbol[meta.type]).toBe('symbol');
133
+ expect(react_element[meta.name]).toBe('span');
134
+ expect(react_element[meta.type]).toBe('react_element');
135
+ expect(array_buffer[meta.meta].length).toBe(3);
136
+ expect(array_buffer[meta.name]).toBe('ArrayBuffer');
137
+ expect(array_buffer[meta.type]).toBe('array_buffer');
138
+ expect(typed_array[meta.meta].length).toBe(3);
139
+ expect(typed_array[meta.name]).toBe('Uint8Array');
140
+ expect(typed_array[meta.type]).toBe('typed_array');
141
+ expect(date[meta.type]).toBe('date');
142
+
143
+ done();
144
+ });
145
+
146
+ it('should support custom objects with enumerable properties and getters', async done => {
147
+ class CustomData {
148
+ _number = 42;
149
+ get number() {
150
+ return this._number;
151
+ }
152
+ set number(value) {
153
+ this._number = value;
154
+ }
155
+ }
156
+
157
+ const descriptor = ((Object.getOwnPropertyDescriptor(
158
+ CustomData.prototype,
159
+ 'number'
160
+ ): any): PropertyDescriptor<number>);
161
+ descriptor.enumerable = true;
162
+ Object.defineProperty(CustomData.prototype, 'number', descriptor);
163
+
164
+ const Example = ({ data }) => null;
165
+
166
+ act(() =>
167
+ ReactDOM.render(
168
+ <Example data={new CustomData()} />,
169
+ document.createElement('div')
170
+ )
171
+ );
172
+
173
+ const id = ((store.getElementIDAtIndex(0): any): number);
174
+ const inspectedElement = await read(id);
175
+
176
+ expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
177
+
178
+ done();
179
+ });
180
+
181
+ it('should not dehydrate nested values until explicitly requested', async done => {
182
+ const Example = () => null;
183
+
184
+ act(() =>
185
+ ReactDOM.render(
186
+ <Example
187
+ nestedObject={{
188
+ a: {
189
+ b: {
190
+ c: [
191
+ {
192
+ d: {
193
+ e: {},
194
+ },
195
+ },
196
+ ],
197
+ },
198
+ },
199
+ }}
200
+ />,
201
+ document.createElement('div')
202
+ )
203
+ );
204
+
205
+ const id = ((store.getElementIDAtIndex(0): any): number);
206
+
207
+ let inspectedElement = await read(id);
208
+ expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
209
+
210
+ inspectedElement = await read(id, ['props', 'nestedObject', 'a']);
211
+ expect(inspectedElement).toMatchSnapshot('2: Inspect props.nestedObject.a');
212
+
213
+ inspectedElement = await read(id, ['props', 'nestedObject', 'a', 'b', 'c']);
214
+ expect(inspectedElement).toMatchSnapshot(
215
+ '3: Inspect props.nestedObject.a.b.c'
216
+ );
217
+
218
+ inspectedElement = await read(id, [
219
+ 'props',
220
+ 'nestedObject',
221
+ 'a',
222
+ 'b',
223
+ 'c',
224
+ 0,
225
+ 'd',
226
+ ]);
227
+ expect(inspectedElement).toMatchSnapshot(
228
+ '4: Inspect props.nestedObject.a.b.c.0.d'
229
+ );
230
+
231
+ done();
232
+ });
233
+});
src/backend/agent.js
+8
-2
@@ -42,6 +42,12 @@ type ElementAndRendererID = {|
42
rendererID: number,
43
|};
44
45
+type InspectElementParams = {|
46
+ id: number,
47
+ path?: Array<string | number>,
48
+ rendererID: number,
49
+|};
50
+
51
type OverrideHookParams = {|
52
id: number,
53
hookID: number,
@@ -242,12 +248,12 @@ export default class Agent extends EventEmitter<{|
248
}
249
};
250
245
- inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
251
+ inspectElement = ({ id, path, rendererID }: InspectElementParams) => {
252
const renderer = this._rendererInterfaces[rendererID];
253
if (renderer == null) {
254
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
255
} else {
250
- this._bridge.send('inspectedElement', renderer.inspectElement(id));
256
+ this._bridge.send('inspectedElement', renderer.inspectElement(id, path));
257
}
258
};
259
src/backend/legacy/renderer.js
+52
-9
@@ -21,6 +21,7 @@ import { decorateMany, forceUpdate, restoreMany } from './utils';
21
import type {
22
DevToolsHook,
23
GetFiberIDForNative,
24
+ InspectedElementPayload,
25
NativeType,
26
PathFrame,
27
PathMatch,
@@ -548,16 +549,58 @@ export function attach(
549
return stringID;
550
}
551
551
- function inspectElement(id: number): InspectedElement | null {
552
- let result = inspectElementRaw(id);
553
- if (result === null) {
554
- return null;
552
+ let currentlyInspectedElementID: number | null = null;
553
+ let currentlyInspectedPaths: Object = {};
554
+
555
+ function mergeInspectedPaths(path: Array<string | number>) {
556
+ let current = currentlyInspectedPaths;
557
+ path.forEach(key => {
558
+ if (!current[key]) {
559
+ current[key] = {};
560
+ }
561
+ current = current[key];
562
+ });
563
+ }
564
+
565
+ function inspectElement(
566
+ id: number,
567
+ path?: Array<string | number>
568
+ ): InspectedElementPayload {
569
+ if (currentlyInspectedElementID !== id) {
570
+ currentlyInspectedElementID = id;
571
+ currentlyInspectedPaths = {};
572
+ }
573
+
574
+ const inspectedElement = inspectElementRaw(id);
575
+ if (inspectedElement === null) {
576
+ return {
577
+ id,
578
+ type: 'not-found',
579
+ };
580
+ }
581
+
582
+ if (path != null) {
583
+ mergeInspectedPaths(path);
584
}
556
- // TODO Review sanitization approach for the below inspectable values.
557
- result.context = cleanForBridge(result.context);
558
- result.props = cleanForBridge(result.props);
559
- result.state = cleanForBridge(result.state);
560
- return result;
585
+
586
+ inspectedElement.context = cleanForBridge(
587
+ inspectedElement.context,
588
+ currentlyInspectedPaths['context']
589
+ );
590
+ inspectedElement.props = cleanForBridge(
591
+ inspectedElement.props,
592
+ currentlyInspectedPaths['props']
593
+ );
594
+ inspectedElement.state = cleanForBridge(
595
+ inspectedElement.state,
596
+ currentlyInspectedPaths['state']
597
+ );
598
+
599
+ return {
600
+ id,
601
+ type: 'full-data',
602
+ value: inspectedElement,
603
+ };
604
}
605
606
function inspectElementRaw(id: number): InspectedElement | null {
src/backend/renderer.js
+110
-26
@@ -22,11 +22,13 @@ import {
22
import {
23
getDisplayName,
24
getDefaultComponentFilters,
25
+ getInObject,
26
getUID,
27
+ setInObject,
28
utfEncodeString,
29
} from 'src/utils';
30
import { sessionStorageGetItem } from 'src/storage';
29
-import { cleanForBridge, copyWithSet, setInObject } from './utils';
31
+import { cleanForBridge, copyWithSet } from './utils';
32
import {
33
__DEBUG__,
34
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
@@ -44,6 +46,7 @@ import type {
46
DevToolsHook,
47
Fiber,
48
InspectedElement,
49
+ InspectedElementPayload,
50
Owner,
51
PathFrame,
52
PathMatch,
@@ -1297,8 +1300,8 @@ export function attach(
1300
}
1301
1302
if (
1300
- mostRecentlyInspectedElementID !== null &&
1301
- mostRecentlyInspectedElementID ===
1303
+ mostRecentlyInspectedElement !== null &&
1304
+ mostRecentlyInspectedElement.id ===
1305
getFiberID(getPrimaryFiber(nextFiber)) &&
1306
didFiberRender(prevFiber, nextFiber)
1307
) {
@@ -2122,37 +2125,118 @@ export function attach(
2125
};
2126
}
2127
2125
- let mostRecentlyInspectedElementID: number | null = null;
2128
+ let mostRecentlyInspectedElement: InspectedElement | null = null;
2129
let hasElementUpdatedSinceLastInspected: boolean = false;
2130
+ let currentlyInspectedPaths: Object = {};
2131
2128
- function inspectElement(id: number): InspectedElement | number | null {
2129
- // If this element has not been updated since it was last inspected, we don't need to re-run it.
2130
- // Instead we can just return the ID to indicate that it has not changed.
2131
- if (
2132
- mostRecentlyInspectedElementID === id &&
2132
+ function isMostRecentlyInspectedElementCurrent(id: number): boolean {
2133
+ return (
2134
+ mostRecentlyInspectedElement !== null &&
2135
+ mostRecentlyInspectedElement.id === id &&
2136
!hasElementUpdatedSinceLastInspected
2134
- ) {
2135
- return id;
2136
- }
2137
+ );
2138
+ }
2139
2138
- mostRecentlyInspectedElementID = id;
2139
- hasElementUpdatedSinceLastInspected = false;
2140
+ function mergeInspectedPaths(path: Array<string | number>) {
2141
+ let current = currentlyInspectedPaths;
2142
+ path.forEach(key => {
2143
+ if (!current[key]) {
2144
+ current[key] = {};
2145
+ }
2146
+ current = current[key];
2147
+ });
2148
+ }
2149
2141
- const inspectedElement = inspectElementRaw(id);
2142
- if (inspectedElement === null) {
2143
- return null;
2144
- }
2145
- inspectedElement.context = cleanForBridge(inspectedElement.context);
2146
- inspectedElement.events = cleanForBridge(inspectedElement.events);
2147
- inspectedElement.hooks = cleanForBridge(inspectedElement.hooks);
2148
- inspectedElement.props = cleanForBridge(inspectedElement.props);
2149
- inspectedElement.state = cleanForBridge(inspectedElement.state);
2150
+ function inspectElement(
2151
+ id: number,
2152
+ path?: Array<string | number>
2153
+ ): InspectedElementPayload {
2154
+ const isCurrent = isMostRecentlyInspectedElementCurrent(id);
2155
+
2156
+ if (isCurrent) {
2157
+ if (path != null) {
2158
+ mergeInspectedPaths(path);
2159
+
2160
+ // If this element has not been updated since it was last inspected,
2161
+ // we can just return the subset of data in the newly-inspected path.
2162
+ return {
2163
+ id,
2164
+ type: 'hydrated-path',
2165
+ path,
2166
+ value: cleanForBridge(
2167
+ getInObject(
2168
+ ((mostRecentlyInspectedElement: any): InspectedElement),
2169
+ path
2170
+ ),
2171
+ mergeInspectedPaths,
2172
+ path
2173
+ ),
2174
+ };
2175
+ } else {
2176
+ // If this element has not been updated since it was last inspected, we don't need to re-run it.
2177
+ // Instead we can just return the ID to indicate that it has not changed.
2178
+ return {
2179
+ id,
2180
+ type: 'no-change',
2181
+ };
2182
+ }
2183
+ } else {
2184
+ hasElementUpdatedSinceLastInspected = false;
2185
+
2186
+ if (
2187
+ mostRecentlyInspectedElement === null ||
2188
+ mostRecentlyInspectedElement.id !== id
2189
+ ) {
2190
+ currentlyInspectedPaths = {};
2191
+ }
2192
+
2193
+ mostRecentlyInspectedElement = inspectElementRaw(id);
2194
+ if (mostRecentlyInspectedElement === null) {
2195
+ return {
2196
+ id,
2197
+ type: 'not-found',
2198
+ };
2199
+ }
2200
2151
- return inspectedElement;
2201
+ if (path != null) {
2202
+ mergeInspectedPaths(path);
2203
+ }
2204
+
2205
+ const cleanedInspectedElement = { ...mostRecentlyInspectedElement };
2206
+
2207
+ cleanedInspectedElement.context = cleanForBridge(
2208
+ cleanedInspectedElement.context,
2209
+ currentlyInspectedPaths.context
2210
+ );
2211
+ cleanedInspectedElement.events = cleanForBridge(
2212
+ cleanedInspectedElement.events,
2213
+ currentlyInspectedPaths.events
2214
+ );
2215
+ cleanedInspectedElement.hooks = cleanForBridge(
2216
+ cleanedInspectedElement.hooks,
2217
+ currentlyInspectedPaths.hooks
2218
+ );
2219
+ cleanedInspectedElement.props = cleanForBridge(
2220
+ cleanedInspectedElement.props,
2221
+ currentlyInspectedPaths.props
2222
+ );
2223
+ cleanedInspectedElement.state = cleanForBridge(
2224
+ cleanedInspectedElement.state,
2225
+ currentlyInspectedPaths.state
2226
+ );
2227
+
2228
+ return {
2229
+ id,
2230
+ type: 'full-data',
2231
+ value: cleanedInspectedElement,
2232
+ };
2233
+ }
2234
}
2235
2236
function logElementToConsole(id) {
2155
- const result = inspectElementRaw(id);
2237
+ const result = isMostRecentlyInspectedElementCurrent(id)
2238
+ ? mostRecentlyInspectedElement
2239
+ : inspectElementRaw(id);
2240
if (result === null) {
2241
console.warn(`Could not find Fiber with id "${id}"`);
2242
return;
@@ -2690,10 +2774,10 @@ export function attach(
2774
2775
return {
2776
cleanup,
2777
+ findNativeNodesForFiberID,
2778
flushInitialOperations,
2779
getBestMatchForTrackedPath,
2780
getFiberIDForNative,
2696
- findNativeNodesForFiberID,
2781
getOwnersList,
2782
getPathForElement,
2783
getProfilingData,
src/backend/types.js
+38
-1
@@ -215,6 +215,40 @@ export type InspectedElement = {|
215
type: ElementType,
216
|};
217
218
+export const InspectElementFullDataType = 'full-data';
219
+export const InspectElementNoChangeType = 'no-change';
220
+export const InspectElementNotFoundType = 'not-found';
221
+export const InspectElementHydratedPathType = 'hydrated-path';
222
+
223
+type InspectElementFullData = {|
224
+ id: number,
225
+ type: 'full-data',
226
+ value: InspectedElement,
227
+|};
228
+
229
+type InspectElementHydratedPath = {|
230
+ id: number,
231
+ type: 'hydrated-path',
232
+ path: Array<string | number>,
233
+ value: any,
234
+|};
235
+
236
+type InspectElementNoChange = {|
237
+ id: number,
238
+ type: 'no-change',
239
+|};
240
+
241
+type InspectElementNotFound = {|
242
+ id: number,
243
+ type: 'not-found',
244
+|};
245
+
246
+export type InspectedElementPayload =
247
+ | InspectElementFullData
248
+ | InspectElementHydratedPath
249
+ | InspectElementNoChange
250
+ | InspectElementNotFound;
251
+
252
export type RendererInterface = {
253
cleanup: () => void,
254
findNativeNodesForFiberID: FindNativeNodesForFiberID,
@@ -226,7 +260,10 @@ export type RendererInterface = {
260
getPathForElement: (id: number) => Array<PathFrame> | null,
261
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
262
handleCommitFiberUnmount: (fiber: Object) => void,
229
- inspectElement: (id: number) => InspectedElement | number | null,
263
+ inspectElement: (
264
+ id: number,
265
+ path?: Array<string | number>
266
+ ) => InspectedElementPayload,
267
logElementToConsole: (id: number) => void,
268
overrideSuspense: (id: number, forceFallback: boolean) => void,
269
prepareViewElementSource: (id: number) => void,
src/backend/utils.js
+6
-20
@@ -4,12 +4,16 @@ import { dehydrate } from '../hydration';
4
5
import type { DehydratedData } from 'src/devtools/views/Components/types';
6
7
-export function cleanForBridge(data: Object | null): DehydratedData | null {
7
+export function cleanForBridge(
8
+ data: Object | null,
9
+ inspectedPaths?: Object = {},
10
+ path?: Array<string | number> = []
11
+): DehydratedData | null {
12
if (data !== null) {
13
const cleaned = [];
14
15
return {
12
- data: dehydrate(data, cleaned),
16
+ data: dehydrate(data, cleaned, path, inspectedPaths),
17
cleaned,
18
};
19
} else {
@@ -32,21 +36,3 @@ export function copyWithSet(
36
updated[key] = copyWithSet(obj[key], path, value, index + 1);
37
return updated;
38
}
35
-
36
-export function setInObject(
37
- object: Object,
38
- path: Array<string | number>,
39
- value: any
40
-) {
41
- const last = path.pop();
42
- if (object != null) {
43
- const parent: Object = path.reduce(
44
- // $FlowFixMe
45
- (reduced, attribute) => reduced[attribute],
46
- object
47
- );
48
- if (parent) {
49
- parent[last] = value;
50
- }
51
- }
52
-}
src/bridge.js
+8
-3
@@ -4,7 +4,7 @@ import EventEmitter from 'events';
4
5
import type { ComponentFilter, Wall } from './types';
6
import type {
7
- InspectedElement,
7
+ InspectedElementPayload,
8
OwnersList,
9
ProfilingDataBackend,
10
RendererID,
@@ -43,6 +43,11 @@ type OverrideSuspense = {|
43
forceFallback: boolean,
44
|};
45
46
+type InspectElementParams = {|
47
+ ...ElementAndRendererID,
48
+ path?: Array<string | number>,
49
+|};
50
+
51
export default class Bridge extends EventEmitter<{|
52
captureScreenshot: [{| commitIndex: number, rootID: number |}],
53
clearHighlightedElementInDOM: [],
@@ -51,8 +56,8 @@ export default class Bridge extends EventEmitter<{|
56
getProfilingStatus: [],
57
highlightElementInDOM: [HighlightElementInDOM],
58
init: [],
54
- inspectElement: [ElementAndRendererID],
55
- inspectedElement: [InspectedElement | number | null],
59
+ inspectElement: [InspectElementParams],
60
+ inspectedElement: [InspectedElementPayload],
61
isBackendStorageAPISupported: [boolean],
62
logElementToConsole: [ElementAndRendererID],
63
operations: [Uint32Array],
src/devtools/views/Components/HooksTree.js
+1
-1
@@ -86,7 +86,7 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
86
87
if (hook.hasOwnProperty(meta.inspected)) {
88
// This Hook is too deep and hasn't been hydrated.
89
- // TODO: show UI to load its data.
89
+ // TODO (hydration) show UI to load its data.
90
return (
91
<div className={styles.Hook}>
92
<div className={styles.NameValueRow}>
src/devtools/views/Components/InspectedElementContext.js
+142
-75
@@ -8,13 +8,17 @@ import React, {
8
useMemo,
9
useState,
10
} from 'react';
11
+import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
12
import { createResource } from '../../cache';
13
import { BridgeContext, StoreContext } from '../context';
13
-import { hydrate } from 'src/hydration';
14
+import { hydrate, fillInPath } from 'src/hydration';
15
import { TreeStateContext } from './TreeContext';
16
import { separateDisplayNameAndHOCs } from 'src/utils';
17
17
-import type { InspectedElement as InspectedElementBackend } from 'src/backend/types';
18
+import type {
19
+ InspectedElement as InspectedElementBackend,
20
+ InspectedElementPayload,
21
+} from 'src/backend/types';
22
import type {
23
DehydratedData,
24
Element,
@@ -22,8 +26,12 @@ import type {
26
} from 'src/devtools/views/Components/types';
27
import type { Resource, Thenable } from '../../cache';
28
29
+export type GetPath = (id: number, path: Array<string | number>) => void;
30
+export type Read = (id: number) => InspectedElementFrontend | null;
31
+
32
type Context = {|
26
- read(id: number): InspectedElementFrontend | null,
33
+ getPath: GetPath,
34
+ read: Read,
35
|};
36
37
const InspectedElementContext = createContext<Context>(((null: any): Context));
@@ -68,7 +76,15 @@ function InspectedElementContextController({ children }: Props) {
76
const bridge = useContext(BridgeContext);
77
const store = useContext(StoreContext);
78
71
- const read = useCallback(
79
+ const getPath = useCallback<GetPath>(
80
+ (id: number, path: Array<string | number>) => {
81
+ const rendererID = store.getRendererIDForElement(id);
82
+ bridge.send('inspectElement', { id, path, rendererID });
83
+ },
84
+ [bridge, store]
85
+ );
86
+
87
+ const read = useCallback<Read>(
88
(id: number) => {
89
const element = store.getElementByID(id);
90
if (element !== null) {
@@ -85,72 +101,117 @@ function InspectedElementContextController({ children }: Props) {
101
// would itself be blocked by the same render that suspends (waiting for the data).
102
const { selectedElementID } = useContext(TreeStateContext);
103
88
- const [count, setCount] = useState<number>(0);
104
+ const [
105
+ currentlyInspectedElement,
106
+ setCurrentlyInspectedElement,
107
+ ] = useState<InspectedElementFrontend | null>(null);
108
109
// This effect handler invalidates the suspense cache and schedules rendering updates with React.
110
useEffect(() => {
92
- const onInspectedElement = (
93
- data: InspectedElementBackend | number | null
94
- ) => {
95
- // A null value means that the element no longer exists in the backend.
96
- // If it's the same element that's currently selected, that selection will be removed once the Store updates.
97
- // If it's not- then we can just ignore it anyway.
98
- // Either way there is nothing we need to do in this case.
99
- // A numeric value indicates that the element hasn't changed since we last requested its data,
100
- // in which case we don't need to invalidate the cache and re-render anything in the DevTools.
101
- if (data !== null && typeof data === 'object') {
102
- const id = data.id;
103
-
104
- const inspectedElement: InspectedElementFrontend = {
105
- canEditFunctionProps: data.canEditFunctionProps,
106
- canEditHooks: data.canEditHooks,
107
- canToggleSuspense: data.canToggleSuspense,
108
- canViewSource: data.canViewSource,
109
- id: data.id,
110
- source: data.source,
111
- type: data.type,
112
- owners:
113
- data.owners === null
114
- ? null
115
- : data.owners.map(owner => {
116
- const [
117
- displayName,
118
- hocDisplayNames,
119
- ] = separateDisplayNameAndHOCs(owner.displayName, owner.type);
120
- return {
121
- ...owner,
122
- displayName,
123
- hocDisplayNames,
124
- };
125
- }),
126
- context: hydrateHelper(data.context),
127
- events: hydrateHelper(data.events),
128
- hooks: hydrateHelper(data.hooks),
129
- props: hydrateHelper(data.props),
130
- state: hydrateHelper(data.state),
131
- };
132
-
133
- const element = store.getElementByID(id);
134
- if (element !== null) {
135
- const request = inProgressRequests.get(element);
136
- if (request != null) {
137
- inProgressRequests.delete(element);
138
- request.resolveFn(inspectedElement);
139
- } else {
140
- resource.write(element, inspectedElement);
141
-
142
- // Schedule update with React if the curently-selected element has been invalidated.
143
- if (id === selectedElementID) {
144
- setCount(count => count + 1);
111
+ const onInspectedElement = (data: InspectedElementPayload) => {
112
+ const { id } = data;
113
+
114
+ let element;
115
+
116
+ switch (data.type) {
117
+ case 'no-change':
118
+ case 'not-found':
119
+ // No-op
120
+ break;
121
+ case 'hydrated-path':
122
+ // Merge new data into previous object and invalidate cache
123
+ element = store.getElementByID(id);
124
+ if (element !== null) {
125
+ if (currentlyInspectedElement != null) {
126
+ const value = hydrateHelper(data.value, data.path);
127
+ const inspectedElement = { ...currentlyInspectedElement };
128
+
129
+ fillInPath(inspectedElement, data.path, value);
130
+
131
+ resource.write(element, inspectedElement);
132
+
133
+ // Schedule update with React if the curently-selected element has been invalidated.
134
+ if (id === selectedElementID) {
135
+ setCurrentlyInspectedElement(inspectedElement);
136
+ }
137
+ }
138
+ }
139
+ break;
140
+ case 'full-data':
141
+ const {
142
+ canEditFunctionProps,
143
+ canEditHooks,
144
+ canToggleSuspense,
145
+ canViewSource,
146
+ source,
147
+ type,
148
+ owners,
149
+ context,
150
+ events,
151
+ hooks,
152
+ props,
153
+ state,
154
+ } = ((data.value: any): InspectedElementBackend);
155
+
156
+ const inspectedElement: InspectedElementFrontend = {
157
+ canEditFunctionProps,
158
+ canEditHooks,
159
+ canToggleSuspense,
160
+ canViewSource,
161
+ id,
162
+ source,
163
+ type,
164
+ owners:
165
+ owners === null
166
+ ? null
167
+ : owners.map(owner => {
168
+ const [
169
+ displayName,
170
+ hocDisplayNames,
171
+ ] = separateDisplayNameAndHOCs(
172
+ owner.displayName,
173
+ owner.type
174
+ );
175
+ return {
176
+ ...owner,
177
+ displayName,
178
+ hocDisplayNames,
179
+ };
180
+ }),
181
+ context: hydrateHelper(context),
182
+ events: hydrateHelper(events),
183
+ hooks: hydrateHelper(hooks),
184
+ props: hydrateHelper(props),
185
+ state: hydrateHelper(state),
186
+ };
187
+
188
+ element = store.getElementByID(id);
189
+ if (element !== null) {
190
+ const request = inProgressRequests.get(element);
191
+ if (request != null) {
192
+ inProgressRequests.delete(element);
193
+ batchedUpdates(() => {
194
+ request.resolveFn(inspectedElement);
195
+ setCurrentlyInspectedElement(inspectedElement);
196
+ });
197
+ } else {
198
+ resource.write(element, inspectedElement);
199
+
200
+ // Schedule update with React if the curently-selected element has been invalidated.
201
+ if (id === selectedElementID) {
202
+ setCurrentlyInspectedElement(inspectedElement);
203
+ }
204
}
205
}
147
- }
206
+ break;
207
+ default:
208
+ break;
209
}
210
};
211
212
bridge.addListener('inspectedElement', onInspectedElement);
213
return () => bridge.removeListener('inspectedElement', onInspectedElement);
153
- }, [bridge, selectedElementID, store]);
214
+ }, [bridge, currentlyInspectedElement, selectedElementID, store]);
215
216
// This effect handler polls for updates on the currently selected element.
217
useEffect(() => {
@@ -175,16 +236,10 @@ function InspectedElementContextController({ children }: Props) {
236
// Update the $r variable.
237
bridge.send('selectElement', { id: selectedElementID, rendererID });
238
178
- const onInspectedElement = (
179
- data: InspectedElementBackend | number | null
180
- ) => {
181
- if (data !== null) {
182
- // If this is the element we requested, wait a little bit and then ask for an update.
183
- if (data === selectedElementID) {
184
- timeoutID = setTimeout(sendRequest, 1000);
185
- } else if (typeof data === 'object' && data.id === selectedElementID) {
186
- timeoutID = setTimeout(sendRequest, 1000);
187
- }
239
+ const onInspectedElement = ({ id }: InspectedElementPayload) => {
240
+ // If this is the element we requested, wait a little bit and then ask for another update.
241
+ if (id === selectedElementID) {
242
+ timeoutID = setTimeout(sendRequest, 1000);
243
}
244
};
245
@@ -200,10 +255,10 @@ function InspectedElementContextController({ children }: Props) {
255
}, [bridge, selectedElementID, store]);
256
257
const value = useMemo(
203
- () => ({ read }),
204
- // Count is used to invalidate the cache and schedule an update with React.
258
+ () => ({ getPath, read }),
259
+ // InspectedElement is used to invalidate the cache and schedule an update with React.
260
// eslint-disable-next-line react-hooks/exhaustive-deps
206
- [count, read]
261
+ [currentlyInspectedElement, getPath, read]
262
);
263
264
return (
@@ -213,9 +268,21 @@ function InspectedElementContextController({ children }: Props) {
268
);
269
}
270
216
-function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
271
+function hydrateHelper(
272
+ dehydratedData: DehydratedData | null,
273
+ path?: Array<string | number>
274
+): Object | null {
275
if (dehydratedData !== null) {
218
- return hydrate(dehydratedData.data, dehydratedData.cleaned);
276
+ let { cleaned, data } = dehydratedData;
277
+
278
+ if (path) {
279
+ const { length } = path;
280
+ if (length > 0) {
281
+ cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
282
+ }
283
+ }
284
+
285
+ return hydrate(data, cleaned);
286
} else {
287
return null;
288
}
src/devtools/views/Components/KeyValue.css
+3
-1
@@ -21,7 +21,9 @@
21
22
.Value {
23
color: var(--color-attribute-value);
24
- word-break: break-all;
24
+ white-space: nowrap;
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
}
28
29
.None {
src/devtools/views/Components/KeyValue.js
+4
@@ -19,6 +19,9 @@ type KeyValueProps = {|
19
value: any,
20
|};
21
22
+// TODO (hydration) Don't display meta objects.
23
+// Add event listener to request a "read" instead.
24
+
25
export default function KeyValue({
26
depth,
27
hidden,
@@ -78,6 +81,7 @@ export default function KeyValue({
81
</div>
82
);
83
} else if (value.hasOwnProperty(meta.type)) {
84
+ // TODO (hydration) show UI to load its data?
85
// TODO Is this type even necessary? Can we just drop it?
86
children = (
87
<div key="root" className={styles.Item} hidden={hidden} style={style}>
src/devtools/views/Components/types.js
+1
-1
@@ -83,6 +83,6 @@ export type InspectedElement = {|
83
// TODO: Add profiling type
84
85
export type DehydratedData = {|
86
- cleaned: Array<Array<string>>,
86
+ cleaned: Array<Array<string | number>>,
87
data: Object,
88
|};
src/hydration.js
+77
-32
@@ -16,7 +16,7 @@ import {
16
StrictMode,
17
Suspense,
18
} from 'react-is';
19
-import { getDisplayName } from './utils';
19
+import { getDisplayName, getInObject, setInObject } from './utils';
20
21
export const meta = {
22
name: Symbol('name'),
@@ -29,14 +29,12 @@ export const meta = {
29
// This threshold determines the depth at which the bridge "dehydrates" nested data.
30
// Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
31
// unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
32
-// We tried reducing this value from 2 to 1 to improve performance:
33
-// https://github.com/facebook/react-devtools/issues/1200
34
-// But this caused problems with the Profiler's interaction tracing output.
35
-// Because React mutates Fibers, profiling data that is dehydrated for old commits–
36
-// will not be available later from within the Profiler.
37
-// This impacts props/state as well as Interactions.
38
-// https://github.com/facebook/react-devtools/issues/1262
39
-const LEVEL_THRESHOLD = 6;
32
+//
33
+// Reducing this threshold will improve the speed of initial component inspection,
34
+// but may decrease the responsiveness of expanding objects/arrays to inspect further.
35
+//
36
+// Note that reducing the threshold to below two effectively breaks the inspected hooks interface.
37
+const LEVEL_THRESHOLD = 2;
38
39
/**
40
* Get a enhanced/artificial type string based on the object instance
@@ -85,8 +83,8 @@ function getPropType(data: Object): string | null {
83
function createDehydrated(
84
type: string,
85
data: Object,
88
- cleaned: Array<Array<string>>,
89
- path: Array<string>
86
+ cleaned: Array<Array<string | number>>,
87
+ path: Array<string | number>
88
): Object {
89
const meta = {};
90
@@ -109,6 +107,20 @@ function createDehydrated(
107
};
108
}
109
110
+function isInspectedPath(
111
+ path: Array<string | number> = [],
112
+ inspectedPaths: Object
113
+): boolean {
114
+ let current = inspectedPaths;
115
+ for (let i = 0; i < path.length; i++) {
116
+ current = current[path[i]];
117
+ if (!current) {
118
+ return false;
119
+ }
120
+ }
121
+ return true;
122
+}
123
+
124
/**
125
* Strip out complex data (instances, functions, and data nested > LEVEL_THRESHOLD levels deep).
126
* The paths of the stripped out objects are appended to the `cleaned` list.
@@ -129,8 +141,9 @@ function createDehydrated(
141
*/
142
export function dehydrate(
143
data: Object,
132
- cleaned: Array<Array<string>>,
133
- path?: Array<string> = [],
144
+ cleaned: Array<Array<string | number>>,
145
+ path: Array<string | number>,
146
+ inspectedPaths: Object,
147
level?: number = 0
148
): string | Object {
149
const type = getPropType(data);
@@ -185,11 +198,18 @@ export function dehydrate(
198
};
199
200
case 'array':
188
- if (level > LEVEL_THRESHOLD) {
201
+ const arrayPathCheck = isInspectedPath(path, inspectedPaths);
202
+ if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
203
return createDehydrated(type, data, cleaned, path);
204
}
205
return data.map((item, i) =>
192
- dehydrate(item, cleaned, path.concat([i]), level + 1)
206
+ dehydrate(
207
+ item,
208
+ cleaned,
209
+ path.concat([i]),
210
+ inspectedPaths,
211
+ arrayPathCheck ? 1 : level + 1
212
+ )
213
);
214
215
case 'typed_array':
@@ -205,7 +225,8 @@ export function dehydrate(
225
},
226
};
227
case 'object':
208
- if (level > LEVEL_THRESHOLD) {
228
+ const objectPathCheck = isInspectedPath(path, inspectedPaths);
229
+ if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
230
return createDehydrated(type, data, cleaned, path);
231
} else {
232
const res = {};
@@ -214,7 +235,8 @@ export function dehydrate(
235
data[name],
236
cleaned,
237
path.concat([name]),
217
- level + 1
238
+ inspectedPaths,
239
+ objectPathCheck ? 1 : level + 1
240
);
241
}
242
return res;
@@ -225,24 +247,47 @@ export function dehydrate(
247
}
248
}
249
228
-export function hydrate(data: Object, cleaned: Array<Array<string>>): Object {
229
- cleaned.forEach((path: Array<string>) => {
230
- const last = path.pop();
231
- const reduced: Object = path.reduce(
232
- (object: Object, attr: string) => (object ? object[attr] : (null: any)),
233
- data
234
- );
235
- if (!reduced || !reduced[last]) {
250
+export function fillInPath(
251
+ object: Object,
252
+ path: Array<string | number>,
253
+ value: any
254
+) {
255
+ const length = path.length;
256
+ const parent = getInObject(object, path.slice(0, length - 1));
257
+ if (object != null) {
258
+ delete parent[meta.name];
259
+ delete parent[meta.type];
260
+ delete parent[meta.meta];
261
+ delete parent[meta.inspected];
262
+
263
+ setInObject(object, path, value);
264
+ }
265
+}
266
+
267
+export function hydrate(
268
+ object: Object,
269
+ cleaned: Array<Array<string | number>>
270
+): Object {
271
+ cleaned.forEach((path: Array<string | number>) => {
272
+ const length = path.length;
273
+ const last = path[length - 1];
274
+ const parent = getInObject(object, path.slice(0, length - 1));
275
+ if (!parent || !parent[last]) {
276
return;
277
}
238
- const replace: { [key: Symbol]: boolean | string } = {};
239
- replace[meta.name] = reduced[last].name;
240
- replace[meta.type] = reduced[last].type;
241
- replace[meta.meta] = reduced[last].meta;
242
- replace[meta.inspected] = false;
243
- reduced[last] = replace;
278
+
279
+ const value = parent[last];
280
+
281
+ // Replace the string keys with Symbols so they're non-enumerable.
282
+ const replaced: { [key: Symbol]: boolean | string } = {};
283
+ replaced[meta.inspected] = false;
284
+ replaced[meta.meta] = value.meta;
285
+ replaced[meta.name] = value.name;
286
+ replaced[meta.type] = value.type;
287
+
288
+ parent[last] = replaced;
289
});
245
- return data;
290
+ return object;
291
}
292
293
export function getDisplayNameForReactElement(
src/utils.js
+27
@@ -245,3 +245,30 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
245
}
246
return false;
247
}
248
+
249
+export function getInObject(object: Object, path: Array<string | number>): any {
250
+ return path.reduce((reduced: Object, attr: string | number): any => {
251
+ if (typeof reduced === 'object' && reduced !== null) {
252
+ return reduced[attr];
253
+ } else if (Array.isArray(reduced)) {
254
+ return reduced[attr];
255
+ } else {
256
+ return null;
257
+ }
258
+ }, object);
259
+}
260
+
261
+export function setInObject(
262
+ object: Object,
263
+ path: Array<string | number>,
264
+ value: any
265
+) {
266
+ const length = path.length;
267
+ const last = path[length - 1];
268
+ if (object != null) {
269
+ const parent = getInObject(object, path.slice(0, length - 1));
270
+ if (parent) {
271
+ parent[last] = value;
272
+ }
273
+ }
274
+}