@samitouri / QOS-React-2 / commits / a6d3f30f95

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