Tidied up. Added comments. Renamed a few things.
Brian Vaughn committed
Jun 18, 2019 at 08:40 UTC
a56fd4c36dd7cbfca5a02fbe0389c3c7ff9f0100
9 files changed
+99
-66
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+2
@@ -369,6 +369,7 @@ exports[`InspectedElementContext should not tear if hydration is requested after
369
"hooks": null,
370
"props": {
371
"nestedObject": {
372
+ "value": 1,
373
"a": {}
374
}
375
},
@@ -385,6 +386,7 @@ exports[`InspectedElementContext should not tear if hydration is requested after
386
"hooks": null,
387
"props": {
388
"nestedObject": {
389
+ "value": 2,
390
"a": {
391
"value": 2,
392
"b": {
src/__tests__/inspectedElementContext-test.js
+41
-31
@@ -1,7 +1,7 @@
1
// @flow
2
3
import typeof ReactTestRenderer from 'react-test-renderer';
4
-import type { GetPath } from 'src/devtools/views/Components/InspectedElementContext';
4
+import type { GetInspectedElementPath } from 'src/devtools/views/Components/InspectedElementContext';
5
import type Bridge from 'src/bridge';
6
import type Store from 'src/devtools/store';
7
@@ -81,8 +81,8 @@ describe('InspectedElementContext', () => {
81
let didFinish = false;
82
83
function Suspender({ target }) {
84
- const { read } = React.useContext(InspectedElementContext);
85
- const inspectedElement = read(id);
84
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
85
+ const inspectedElement = getInspectedElement(id);
86
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
87
didFinish = true;
88
return null;
@@ -121,8 +121,8 @@ describe('InspectedElementContext', () => {
121
let inspectedElement = null;
122
123
function Suspender({ target }) {
124
- const { read } = React.useContext(InspectedElementContext);
125
- inspectedElement = read(id);
124
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
125
+ inspectedElement = getInspectedElement(id);
126
return null;
127
}
128
@@ -189,8 +189,8 @@ describe('InspectedElementContext', () => {
189
let inspectedElement = null;
190
191
function Suspender({ target }) {
192
- const { read } = React.useContext(InspectedElementContext);
193
- inspectedElement = read(target);
192
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
193
+ inspectedElement = getInspectedElement(target);
194
return null;
195
}
196
@@ -283,8 +283,8 @@ describe('InspectedElementContext', () => {
283
let inspectedElement = null;
284
285
function Suspender({ target }) {
286
- const { read } = React.useContext(InspectedElementContext);
287
- inspectedElement = read(id);
286
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
287
+ inspectedElement = getInspectedElement(id);
288
return null;
289
}
290
@@ -371,8 +371,8 @@ describe('InspectedElementContext', () => {
371
let didFinish = false;
372
373
function Suspender({ target }) {
374
- const { read } = React.useContext(InspectedElementContext);
375
- const inspectedElement = read(id);
374
+ const { getInspectedElement } = React.useContext(InspectedElementContext);
375
+ const inspectedElement = getInspectedElement(id);
376
expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
377
didFinish = true;
378
return null;
@@ -434,13 +434,13 @@ describe('InspectedElementContext', () => {
434
435
const id = ((store.getElementIDAtIndex(0): any): number);
436
437
- let getPath: GetPath = ((null: any): GetPath);
437
+ let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
438
let inspectedElement = null;
439
440
function Suspender({ target }) {
441
const context = React.useContext(InspectedElementContext);
442
- getPath = context.getPath;
443
- inspectedElement = context.read(target);
442
+ getInspectedElementPath = context.getInspectedElementPath;
443
+ inspectedElement = context.getInspectedElement(target);
444
return null;
445
}
446
@@ -458,13 +458,13 @@ describe('InspectedElementContext', () => {
458
),
459
false
460
);
461
- expect(getPath).not.toBeNull();
461
+ expect(getInspectedElementPath).not.toBeNull();
462
expect(inspectedElement).not.toBeNull();
463
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
464
465
inspectedElement = null;
466
TestUtils.act(() => {
467
- getPath(id, ['props', 'nestedObject', 'a']);
467
+ getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
468
jest.runOnlyPendingTimers();
469
});
470
expect(inspectedElement).not.toBeNull();
@@ -472,7 +472,7 @@ describe('InspectedElementContext', () => {
472
473
inspectedElement = null;
474
TestUtils.act(() => {
475
- getPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
475
+ getInspectedElementPath(id, ['props', 'nestedObject', 'a', 'b', 'c']);
476
jest.runOnlyPendingTimers();
477
});
478
expect(inspectedElement).not.toBeNull();
@@ -482,7 +482,15 @@ describe('InspectedElementContext', () => {
482
483
inspectedElement = null;
484
TestUtils.act(() => {
485
- getPath(id, ['props', 'nestedObject', 'a', 'b', 'c', 0, 'd']);
485
+ getInspectedElementPath(id, [
486
+ 'props',
487
+ 'nestedObject',
488
+ 'a',
489
+ 'b',
490
+ 'c',
491
+ 0,
492
+ 'd',
493
+ ]);
494
jest.runOnlyPendingTimers();
495
});
496
expect(inspectedElement).not.toBeNull();
@@ -492,7 +500,7 @@ describe('InspectedElementContext', () => {
500
501
inspectedElement = null;
502
TestUtils.act(() => {
495
- getPath(id, ['hooks', 0, 'value']);
503
+ getInspectedElementPath(id, ['hooks', 0, 'value']);
504
jest.runOnlyPendingTimers();
505
});
506
expect(inspectedElement).not.toBeNull();
@@ -500,7 +508,7 @@ describe('InspectedElementContext', () => {
508
509
inspectedElement = null;
510
TestUtils.act(() => {
503
- getPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
511
+ getInspectedElementPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
512
jest.runOnlyPendingTimers();
513
});
514
expect(inspectedElement).not.toBeNull();
@@ -542,13 +550,13 @@ describe('InspectedElementContext', () => {
550
551
const id = ((store.getElementIDAtIndex(0): any): number);
552
545
- let getPath: GetPath = ((null: any): GetPath);
553
+ let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
554
let inspectedElement = null;
555
556
function Suspender({ target }) {
557
const context = React.useContext(InspectedElementContext);
550
- getPath = context.getPath;
551
- inspectedElement = context.read(id);
558
+ getInspectedElementPath = context.getInspectedElementPath;
559
+ inspectedElement = context.getInspectedElement(id);
560
return null;
561
}
562
@@ -566,13 +574,13 @@ describe('InspectedElementContext', () => {
574
),
575
false
576
);
569
- expect(getPath).not.toBeNull();
577
+ expect(getInspectedElementPath).not.toBeNull();
578
expect(inspectedElement).not.toBeNull();
579
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
580
581
inspectedElement = null;
582
TestUtils.act(() => {
575
- getPath(id, ['props', 'nestedObject', 'a']);
583
+ getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
584
jest.runOnlyPendingTimers();
585
});
586
expect(inspectedElement).not.toBeNull();
@@ -580,7 +588,7 @@ describe('InspectedElementContext', () => {
588
589
inspectedElement = null;
590
TestUtils.act(() => {
583
- getPath(id, ['props', 'nestedObject', 'c']);
591
+ getInspectedElementPath(id, ['props', 'nestedObject', 'c']);
592
jest.runOnlyPendingTimers();
593
});
594
expect(inspectedElement).not.toBeNull();
@@ -629,6 +637,7 @@ describe('InspectedElementContext', () => {
637
ReactDOM.render(
638
<Example
639
nestedObject={{
640
+ value: 1,
641
a: {
642
value: 1,
643
b: {
@@ -643,13 +652,13 @@ describe('InspectedElementContext', () => {
652
653
const id = ((store.getElementIDAtIndex(0): any): number);
654
646
- let getPath: GetPath = ((null: any): GetPath);
655
+ let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
656
let inspectedElement = null;
657
658
function Suspender({ target }) {
659
const context = React.useContext(InspectedElementContext);
651
- getPath = context.getPath;
652
- inspectedElement = context.read(id);
660
+ getInspectedElementPath = context.getInspectedElementPath;
661
+ inspectedElement = context.getInspectedElement(id);
662
return null;
663
}
664
@@ -667,7 +676,7 @@ describe('InspectedElementContext', () => {
676
),
677
false
678
);
670
- expect(getPath).not.toBeNull();
679
+ expect(getInspectedElementPath).not.toBeNull();
680
expect(inspectedElement).not.toBeNull();
681
expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
682
@@ -675,6 +684,7 @@ describe('InspectedElementContext', () => {
684
ReactDOM.render(
685
<Example
686
nestedObject={{
687
+ value: 2,
688
a: {
689
value: 2,
690
b: {
@@ -689,7 +699,7 @@ describe('InspectedElementContext', () => {
699
700
inspectedElement = null;
701
TestUtils.act(() => {
692
- getPath(id, ['props', 'nestedObject', 'a']);
702
+ getInspectedElementPath(id, ['props', 'nestedObject', 'a']);
703
jest.runOnlyPendingTimers();
704
});
705
expect(inspectedElement).not.toBeNull();
src/backend/legacy/renderer.js
+4
@@ -552,6 +552,8 @@ export function attach(
552
let currentlyInspectedElementID: number | null = null;
553
let currentlyInspectedPaths: Object = {};
554
555
+ // Track the intersection of currently inspected paths,
556
+ // so that we can send their data along if the element is re-rendered.
557
function mergeInspectedPaths(path: Array<string | number>) {
558
let current = currentlyInspectedPaths;
559
path.forEach(key => {
@@ -563,6 +565,8 @@ export function attach(
565
}
566
567
function createIsPathWhitelisted(key: string) {
568
+ // This function helps prevent previously-inspected paths from being dehydrated in updates.
569
+ // This is important to avoid a bad user experience where expanded toggles collapse on update.
570
return function isPathWhitelisted(path: Array<string | number>): boolean {
571
let current = currentlyInspectedPaths[key];
572
if (!current) {
src/backend/renderer.js
+9
-2
@@ -2137,6 +2137,8 @@ export function attach(
2137
);
2138
}
2139
2140
+ // Track the intersection of currently inspected paths,
2141
+ // so that we can send their data along if the element is re-rendered.
2142
function mergeInspectedPaths(path: Array<string | number>) {
2143
let current = currentlyInspectedPaths;
2144
path.forEach(key => {
@@ -2148,9 +2150,13 @@ export function attach(
2150
}
2151
2152
function createIsPathWhitelisted(isHooksPath: boolean, key: string | null) {
2153
+ // This function helps prevent previously-inspected paths from being dehydrated in updates.
2154
+ // This is important to avoid a bad user experience where expanded toggles collapse on update.
2155
return function isPathWhitelisted(path: Array<string | number>): boolean {
2156
// Dehydrating the 'subHooks' property makes the HooksTree UI a lot more complicated,
2157
// so it's easiest for now if we just don't break on this boundary.
2158
+ // We can always dehydrate a level deeper (in the value object).
2159
+ // TODO (hydration) This check depends on a LEVEL_THRESHOLD of 2 to avoid dehydrating a hook incorrectly.
2160
if (isHooksPath && path[path.length - 1] === 'subHooks') {
2161
return true;
2162
}
@@ -2225,8 +2231,10 @@ export function attach(
2231
mergeInspectedPaths(path);
2232
}
2233
2234
+ // Clone before cleaning so that we preserve the full data.
2235
+ // This will enable us to send patches without re-inspecting if hydrated paths are requested.
2236
+ // (Reducing how often we shallow-render is a better DX for function components that use hooks.)
2237
const cleanedInspectedElement = { ...mostRecentlyInspectedElement };
2229
-
2238
cleanedInspectedElement.context = cleanForBridge(
2239
cleanedInspectedElement.context,
2240
createIsPathWhitelisted(false, 'context')
@@ -2308,7 +2316,6 @@ export function attach(
2316
const fiber = findCurrentFiberUsingSlowPathById(id);
2317
if (fiber !== null) {
2318
if (typeof overrideHookState === 'function') {
2311
- console.log('[renderer] overrideHookState()', { path, value, index });
2319
overrideHookState(fiber, index, path, value);
2320
}
2321
}
src/devtools/views/Components/HooksTree.js
+10
-8
@@ -23,12 +23,12 @@ type HooksTreeViewProps = {|
23
|};
24
25
export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
26
- const { getPath } = useContext(InspectedElementContext);
26
+ const { getInspectedElementPath } = useContext(InspectedElementContext);
27
const inspectPath = useCallback(
28
(path: Array<string | number>) => {
29
- getPath(id, ['hooks', ...path]);
29
+ getInspectedElementPath(id, ['hooks', ...path]);
30
},
31
- [getPath, id]
31
+ [getInspectedElementPath, id]
32
);
33
const handleCopy = useCallback(() => copy(serializeHooksForCopy(hooks)), [
34
hooks,
@@ -114,7 +114,9 @@ function HookView({
114
115
if (hook.hasOwnProperty(meta.inspected)) {
116
// This Hook is too deep and hasn't been hydrated.
117
- // TODO (hydration) show UI to load its data.
117
+ if (__DEV__) {
118
+ console.warn('Unexpected dehydrated hook; this is a DevTools error.');
119
+ }
120
return (
121
<div className={styles.Hook}>
122
<div className={styles.NameValueRow}>
@@ -124,8 +126,6 @@ function HookView({
126
);
127
}
128
127
- // TODO Add click and key handlers for toggling element open/close state.
128
-
129
const isCustomHook = subHooks.length > 0;
130
131
const type = typeof value;
@@ -218,8 +218,10 @@ function HookView({
218
bridge.send('overrideHookState', {
219
id,
220
hookID,
221
- // Hooks override function expects a relative path for the specified hook (id).
222
- // This should not include the fake tree structure DevTools uses for display.
221
+ // Hooks override function expects a relative path for the specified hook (id),
222
+ // starting with its id within the (flat) hooks list structure.
223
+ // This relative path does not include the fake tree structure DevTools uses for display,
224
+ // so it's important that we remove that part of the path before sending the update.
225
path: absolutePath.slice(path.length + 1),
226
rendererID,
227
value,
src/devtools/views/Components/InspectedElementContext.js
+16
-8
@@ -26,12 +26,17 @@ 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;
29
+export type GetInspectedElementPath = (
30
+ id: number,
31
+ path: Array<string | number>
32
+) => void;
33
+export type GetInspectedElement = (
34
+ id: number
35
+) => InspectedElementFrontend | null;
36
37
type Context = {|
33
- getPath: GetPath,
34
- read: Read,
38
+ getInspectedElementPath: GetInspectedElementPath,
39
+ getInspectedElement: GetInspectedElement,
40
|};
41
42
const InspectedElementContext = createContext<Context>(((null: any): Context));
@@ -76,7 +81,8 @@ function InspectedElementContextController({ children }: Props) {
81
const bridge = useContext(BridgeContext);
82
const store = useContext(StoreContext);
83
79
- const getPath = useCallback<GetPath>(
84
+ // Ask the backend to fill in a "dehydrated" path; this will result in a "inspectedElement".
85
+ const getInspectedElementPath = useCallback<GetInspectedElementPath>(
86
(id: number, path: Array<string | number>) => {
87
const rendererID = store.getRendererIDForElement(id);
88
bridge.send('inspectElement', { id, path, rendererID });
@@ -84,7 +90,7 @@ function InspectedElementContextController({ children }: Props) {
90
[bridge, store]
91
);
92
87
- const read = useCallback<Read>(
93
+ const getInspectedElement = useCallback<GetInspectedElement>(
94
(id: number) => {
95
const element = store.getElementByID(id);
96
if (element !== null) {
@@ -266,10 +272,10 @@ function InspectedElementContextController({ children }: Props) {
272
}, [bridge, selectedElementID, store]);
273
274
const value = useMemo(
269
- () => ({ getPath, read }),
275
+ () => ({ getInspectedElement, getInspectedElementPath }),
276
// InspectedElement is used to invalidate the cache and schedule an update with React.
277
// eslint-disable-next-line react-hooks/exhaustive-deps
272
- [currentlyInspectedElement, getPath, read]
278
+ [currentlyInspectedElement, getInspectedElement, getInspectedElementPath]
279
);
280
281
return (
@@ -289,6 +295,8 @@ function hydrateHelper(
295
if (path) {
296
const { length } = path;
297
if (length > 0) {
298
+ // Hydration helper requires full paths, but inspection dehydrates with relative paths.
299
+ // In that event it's important that we adjust the "cleaned" paths to match.
300
cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
301
}
302
}
src/devtools/views/Components/KeyValue.js
-3
@@ -22,9 +22,6 @@ type KeyValueProps = {|
22
value: any,
23
|};
24
25
-// TODO (hydration) Don't display meta objects.
26
-// Add event listener to request a "read" instead.
27
-
25
export default function KeyValue({
26
depth,
27
inspectPath,
src/devtools/views/Components/SelectedElement.js
+15
-13
@@ -25,7 +25,7 @@ import {
25
26
import styles from './SelectedElement.css';
27
28
-import type { GetPath } from './InspectedElementContext';
28
+import type { GetInspectedElementPath } from './InspectedElementContext';
29
import type { Element, InspectedElement } from './types';
30
import type { ElementType } from 'src/types';
31
@@ -39,7 +39,9 @@ export default function SelectedElement(_: Props) {
39
const store = useContext(StoreContext);
40
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
41
42
- const { getPath, read } = useContext(InspectedElementContext);
42
+ const { getInspectedElementPath, getInspectedElement } = useContext(
43
+ InspectedElementContext
44
+ );
45
46
const element =
47
inspectedElementID !== null
@@ -47,7 +49,7 @@ export default function SelectedElement(_: Props) {
49
: null;
50
51
const inspectedElement =
50
- inspectedElementID != null ? read(inspectedElementID) : null;
52
+ inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
53
54
const highlightElement = useCallback(() => {
55
if (element !== null && inspectedElementID !== null) {
@@ -202,10 +204,10 @@ export default function SelectedElement(_: Props) {
204
{inspectedElement !== null && (
205
<InspectedElementView
206
key={
205
- inspectedElementID /* Ensure state resets between seleted Elements */
207
+ inspectedElementID /* Force reset when seleted Element changes */
208
}
209
element={element}
208
- getPath={getPath}
210
+ getInspectedElementPath={getInspectedElementPath}
211
inspectedElement={inspectedElement}
212
/>
213
)}
@@ -217,7 +219,7 @@ export type InspectPath = (path: Array<string | number>) => void;
219
220
type InspectedElementViewProps = {|
221
element: Element,
220
- getPath: GetPath,
222
+ getInspectedElementPath: GetInspectedElementPath,
223
inspectedElement: InspectedElement,
224
|};
225
@@ -225,7 +227,7 @@ const IS_SUSPENDED = 'Suspended';
227
228
function InspectedElementView({
229
element,
228
- getPath,
230
+ getInspectedElementPath,
231
inspectedElement,
232
}: InspectedElementViewProps) {
233
const { id, type } = element;
@@ -247,21 +249,21 @@ function InspectedElementView({
249
250
const inspectContextPath = useCallback(
251
(path: Array<string | number>) => {
250
- getPath(id, ['context', ...path]);
252
+ getInspectedElementPath(id, ['context', ...path]);
253
},
252
- [getPath, id]
254
+ [getInspectedElementPath, id]
255
);
256
const inspectPropsPath = useCallback(
257
(path: Array<string | number>) => {
256
- getPath(id, ['props', ...path]);
258
+ getInspectedElementPath(id, ['props', ...path]);
259
},
258
- [getPath, id]
260
+ [getInspectedElementPath, id]
261
);
262
const inspectStatePath = useCallback(
263
(path: Array<string | number>) => {
262
- getPath(id, ['state', ...path]);
264
+ getInspectedElementPath(id, ['state', ...path]);
265
},
264
- [getPath, id]
266
+ [getInspectedElementPath, id]
267
);
268
269
let overrideContextFn = null;
src/hydration.js
+2
-1
@@ -42,7 +42,8 @@ type Dehydrated = {|
42
// Reducing this threshold will improve the speed of initial component inspection,
43
// but may decrease the responsiveness of expanding objects/arrays to inspect further.
44
//
45
-// Note that reducing the threshold to below two effectively breaks the inspected hooks interface.
45
+// Note that reducing the threshold to below 2 effectively breaks the inspected hooks interface.
46
+// It is only safe to dehydrate hooks within the "value" key, never within the "subHooks" array directly.
47
const LEVEL_THRESHOLD = 2;
48
49
/**