Hooks hydrate/dehydrate
Brian Vaughn committed
Jun 17, 2019 at 14:30 UTC
d3d8a5d8b1fa49a0f61c2596dd81232af9a28908
9 files changed
+328
-56
shells/dev/app/InspectableElements/Hydration.js
+59
-1
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import React from 'react';
3
+import React, { useDebugValue, useState } from 'react';
4
5
const div = document.createElement('div');
6
const exmapleFunction = () => {};
@@ -35,6 +35,61 @@ const objectOfObjects = {
35
qux: {},
36
};
37
38
+function useOuterFoo() {
39
+ useDebugValue({
40
+ debugA: {
41
+ debugB: {
42
+ debugC: 'abc',
43
+ },
44
+ },
45
+ });
46
+ useState({
47
+ valueA: {
48
+ valueB: {
49
+ valueC: 'abc',
50
+ },
51
+ },
52
+ });
53
+ return useInnerFoo();
54
+}
55
+
56
+function useInnerFoo() {
57
+ const [value] = useState([[['a', 'b', 'c']]]);
58
+ return value;
59
+}
60
+
61
+function useOuterBar() {
62
+ useDebugValue({
63
+ debugA: {
64
+ debugB: {
65
+ debugC: 'abc',
66
+ },
67
+ },
68
+ });
69
+ return useInnerBar();
70
+}
71
+
72
+function useInnerBar() {
73
+ useDebugValue({
74
+ debugA: {
75
+ debugB: {
76
+ debugC: 'abc',
77
+ },
78
+ },
79
+ });
80
+ const [count] = useState(123);
81
+ return count;
82
+}
83
+
84
+function useOuterBaz() {
85
+ return useInnerBaz();
86
+}
87
+
88
+function useInnerBaz() {
89
+ const [count] = useState(123);
90
+ return count;
91
+}
92
+
93
export default function Hydration() {
94
return (
95
<ChildComponent
@@ -52,5 +107,8 @@ export default function Hydration() {
107
}
108
109
function ChildComponent(props: any) {
110
+ useOuterFoo();
111
+ useOuterBar();
112
+ useOuterBaz();
113
return null;
114
}
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+117
-7
@@ -107,7 +107,7 @@ exports[`InspectedElementContext should inspect the currently selected element:
107
"isStateEditable": true,
108
"name": "State",
109
"value": 1,
110
- "subHooks": {}
110
+ "subHooks": []
111
}
112
],
113
"props": {
@@ -124,7 +124,15 @@ exports[`InspectedElementContext should not dehydrate nested values until explic
124
"owners": null,
125
"context": null,
126
"events": null,
127
- "hooks": null,
127
+ "hooks": [
128
+ {
129
+ "id": 0,
130
+ "isStateEditable": true,
131
+ "name": "State",
132
+ "value": {},
133
+ "subHooks": []
134
+ }
135
+ ],
136
"props": {
137
"nestedObject": {
138
"a": {}
@@ -140,7 +148,15 @@ exports[`InspectedElementContext should not dehydrate nested values until explic
148
"owners": null,
149
"context": null,
150
"events": null,
143
- "hooks": null,
151
+ "hooks": [
152
+ {
153
+ "id": 0,
154
+ "isStateEditable": true,
155
+ "name": "State",
156
+ "value": {},
157
+ "subHooks": []
158
+ }
159
+ ],
160
"props": {
161
"nestedObject": {
162
"a": {
@@ -160,7 +176,15 @@ exports[`InspectedElementContext should not dehydrate nested values until explic
176
"owners": null,
177
"context": null,
178
"events": null,
163
- "hooks": null,
179
+ "hooks": [
180
+ {
181
+ "id": 0,
182
+ "isStateEditable": true,
183
+ "name": "State",
184
+ "value": {},
185
+ "subHooks": []
186
+ }
187
+ ],
188
"props": {
189
"nestedObject": {
190
"a": {
@@ -184,7 +208,93 @@ exports[`InspectedElementContext should not dehydrate nested values until explic
208
"owners": null,
209
"context": null,
210
"events": null,
187
- "hooks": null,
211
+ "hooks": [
212
+ {
213
+ "id": 0,
214
+ "isStateEditable": true,
215
+ "name": "State",
216
+ "value": {},
217
+ "subHooks": []
218
+ }
219
+ ],
220
+ "props": {
221
+ "nestedObject": {
222
+ "a": {
223
+ "b": {
224
+ "c": [
225
+ {
226
+ "d": {
227
+ "e": {}
228
+ }
229
+ }
230
+ ]
231
+ }
232
+ }
233
+ }
234
+ },
235
+ "state": null
236
+}
237
+`;
238
+
239
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 5: Inspect hooks.0.value 1`] = `
240
+{
241
+ "id": 2,
242
+ "owners": null,
243
+ "context": null,
244
+ "events": null,
245
+ "hooks": [
246
+ {
247
+ "id": 0,
248
+ "isStateEditable": true,
249
+ "name": "State",
250
+ "value": {
251
+ "foo": {
252
+ "bar": {}
253
+ }
254
+ },
255
+ "subHooks": []
256
+ }
257
+ ],
258
+ "props": {
259
+ "nestedObject": {
260
+ "a": {
261
+ "b": {
262
+ "c": [
263
+ {
264
+ "d": {
265
+ "e": {}
266
+ }
267
+ }
268
+ ]
269
+ }
270
+ }
271
+ }
272
+ },
273
+ "state": null
274
+}
275
+`;
276
+
277
+exports[`InspectedElementContext should not dehydrate nested values until explicitly requested: 6: Inspect hooks.0.value.foo.bar 1`] = `
278
+{
279
+ "id": 2,
280
+ "owners": null,
281
+ "context": null,
282
+ "events": null,
283
+ "hooks": [
284
+ {
285
+ "id": 0,
286
+ "isStateEditable": true,
287
+ "name": "State",
288
+ "value": {
289
+ "foo": {
290
+ "bar": {
291
+ "baz": "hi"
292
+ }
293
+ }
294
+ },
295
+ "subHooks": []
296
+ }
297
+ ],
298
"props": {
299
"nestedObject": {
300
"a": {
@@ -216,7 +326,7 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
326
"isStateEditable": true,
327
"name": "State",
328
"value": 0,
219
- "subHooks": {}
329
+ "subHooks": []
330
}
331
],
332
"props": {
@@ -239,7 +349,7 @@ exports[`InspectedElementContext should not re-render a function with hooks if i
349
"isStateEditable": true,
350
"name": "State",
351
"value": 0,
242
- "subHooks": {}
352
+ "subHooks": []
353
}
354
],
355
"props": {
src/__tests__/inspectedElementContext-test.js
+29
-1
@@ -397,7 +397,17 @@ describe('InspectedElementContext', () => {
397
});
398
399
it('should not dehydrate nested values until explicitly requested', async done => {
400
- const Example = () => null;
400
+ const Example = () => {
401
+ const [state] = React.useState({
402
+ foo: {
403
+ bar: {
404
+ baz: 'hi',
405
+ },
406
+ },
407
+ });
408
+
409
+ return state.foo.bar.baz;
410
+ };
411
412
const container = document.createElement('div');
413
await utils.actAsync(() =>
@@ -479,6 +489,24 @@ describe('InspectedElementContext', () => {
489
'4: Inspect props.nestedObject.a.b.c.0.d'
490
);
491
492
+ inspectedElement = null;
493
+ TestUtils.act(() => {
494
+ getPath(id, ['hooks', 0, 'value']);
495
+ jest.runOnlyPendingTimers();
496
+ });
497
+ expect(inspectedElement).not.toBeNull();
498
+ expect(inspectedElement).toMatchSnapshot('5: Inspect hooks.0.value');
499
+
500
+ inspectedElement = null;
501
+ TestUtils.act(() => {
502
+ getPath(id, ['hooks', 0, 'value', 'foo', 'bar']);
503
+ jest.runOnlyPendingTimers();
504
+ });
505
+ expect(inspectedElement).not.toBeNull();
506
+ expect(inspectedElement).toMatchSnapshot(
507
+ '6: Inspect hooks.0.value.foo.bar'
508
+ );
509
+
510
done();
511
});
512
src/backend/legacy/renderer.js
+21
-3
@@ -562,6 +562,24 @@ export function attach(
562
});
563
}
564
565
+ function isKeyedPathWhitelisted(
566
+ key: string
567
+ ): (path: Array<string | number>) => boolean {
568
+ return (path: Array<string | number>) =>
569
+ isPathWhitelisted([key].concat(path));
570
+ }
571
+
572
+ function isPathWhitelisted(path: Array<string | number>): boolean {
573
+ let current = currentlyInspectedPaths;
574
+ for (let i = 0; i < path.length; i++) {
575
+ current = current[path[i]];
576
+ if (!current) {
577
+ return false;
578
+ }
579
+ }
580
+ return true;
581
+ }
582
+
583
function inspectElement(
584
id: number,
585
path?: Array<string | number>
@@ -585,15 +603,15 @@ export function attach(
603
604
inspectedElement.context = cleanForBridge(
605
inspectedElement.context,
588
- currentlyInspectedPaths['context']
606
+ isKeyedPathWhitelisted('context')
607
);
608
inspectedElement.props = cleanForBridge(
609
inspectedElement.props,
592
- currentlyInspectedPaths['props']
610
+ isKeyedPathWhitelisted('props')
611
);
612
inspectedElement.state = cleanForBridge(
613
inspectedElement.state,
596
- currentlyInspectedPaths['state']
614
+ isKeyedPathWhitelisted('state')
615
);
616
617
return {
src/backend/renderer.js
+29
-6
@@ -2147,6 +2147,28 @@ export function attach(
2147
});
2148
}
2149
2150
+ function isKeyedPathWhitelisted(
2151
+ key: string,
2152
+ isHooks: boolean
2153
+ ): (path: Array<string | number>) => boolean {
2154
+ return (path: Array<string | number>) =>
2155
+ isPathWhitelisted([key].concat(path)) ||
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
+ (isHooks && path[path.length - 1] === 'subHooks');
2159
+ }
2160
+
2161
+ function isPathWhitelisted(path: Array<string | number>): boolean {
2162
+ let current = currentlyInspectedPaths;
2163
+ for (let i = 0; i < path.length; i++) {
2164
+ current = current[path[i]];
2165
+ if (!current) {
2166
+ return false;
2167
+ }
2168
+ }
2169
+ return true;
2170
+ }
2171
+
2172
function inspectElement(
2173
id: number,
2174
path?: Array<string | number>
@@ -2168,7 +2190,7 @@ export function attach(
2190
((mostRecentlyInspectedElement: any): InspectedElement),
2191
path
2192
),
2171
- currentlyInspectedPaths,
2193
+ isPathWhitelisted,
2194
path
2195
),
2196
};
@@ -2206,23 +2228,23 @@ export function attach(
2228
2229
cleanedInspectedElement.context = cleanForBridge(
2230
cleanedInspectedElement.context,
2209
- currentlyInspectedPaths.context
2231
+ isKeyedPathWhitelisted('context', false)
2232
);
2233
cleanedInspectedElement.events = cleanForBridge(
2234
cleanedInspectedElement.events,
2213
- currentlyInspectedPaths.events
2235
+ isKeyedPathWhitelisted('events', false)
2236
);
2237
cleanedInspectedElement.hooks = cleanForBridge(
2238
cleanedInspectedElement.hooks,
2217
- currentlyInspectedPaths.hooks
2239
+ isKeyedPathWhitelisted('hooks', true)
2240
);
2241
cleanedInspectedElement.props = cleanForBridge(
2242
cleanedInspectedElement.props,
2221
- currentlyInspectedPaths.props
2243
+ isKeyedPathWhitelisted('props', false)
2244
);
2245
cleanedInspectedElement.state = cleanForBridge(
2246
cleanedInspectedElement.state,
2225
- currentlyInspectedPaths.state
2247
+ isKeyedPathWhitelisted('state', false)
2248
);
2249
2250
return {
@@ -2285,6 +2307,7 @@ export function attach(
2307
const fiber = findCurrentFiberUsingSlowPathById(id);
2308
if (fiber !== null) {
2309
if (typeof overrideHookState === 'function') {
2310
+ console.log('[renderer] overrideHookState()', { path, value, index });
2311
overrideHookState(fiber, index, path, value);
2312
}
2313
}
src/backend/utils.js
+3
-2
@@ -6,14 +6,14 @@ import type { DehydratedData } from 'src/devtools/views/Components/types';
6
7
export function cleanForBridge(
8
data: Object | null,
9
- inspectedPaths?: Object = {},
9
+ isPathWhitelisted: (path: Array<string | number>) => boolean,
10
path?: Array<string | number> = []
11
): DehydratedData | null {
12
if (data !== null) {
13
const cleaned = [];
14
15
return {
16
- data: dehydrate(data, cleaned, path, inspectedPaths),
16
+ data: dehydrate(data, cleaned, path, isPathWhitelisted),
17
cleaned,
18
};
19
} else {
@@ -27,6 +27,7 @@ export function copyWithSet(
27
value: any,
28
index: number = 0
29
): Object | Array<any> {
30
+ console.log('[utils] copyWithSet()', obj, path, index, value);
31
if (index >= path.length) {
32
return value;
33
}
src/devtools/views/Components/HooksTree.js
+63
-15
@@ -7,11 +7,13 @@ import Button from '../Button';
7
import ButtonIcon from '../ButtonIcon';
8
import EditableValue from './EditableValue';
9
import ExpandCollapseToggle from './ExpandCollapseToggle';
10
+import { InspectedElementContext } from './InspectedElementContext';
11
import KeyValue from './KeyValue';
12
import { serializeHooksForCopy } from '../utils';
13
import styles from './HooksTree.css';
14
import { meta } from '../../../hydration';
15
16
+import type { InspectPath } from './SelectedElement';
17
import type { HooksNode, HooksTree } from 'src/backend/types';
18
19
type HooksTreeViewProps = {|
@@ -21,6 +23,13 @@ type HooksTreeViewProps = {|
23
|};
24
25
export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
26
+ const { getPath } = useContext(InspectedElementContext);
27
+ const inspectPath = useCallback(
28
+ (path: Array<string | number>) => {
29
+ getPath(id, ['hooks', ...path]);
30
+ },
31
+ [getPath, id]
32
+ );
33
const handleCopy = useCallback(() => copy(serializeHooksForCopy(hooks)), [
34
hooks,
35
]);
@@ -36,7 +45,13 @@ export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
45
<ButtonIcon type="copy" />
46
</Button>
47
</div>
39
- <InnerHooksTreeView canEditHooks={canEditHooks} hooks={hooks} id={id} />
48
+ <InnerHooksTreeView
49
+ canEditHooks={canEditHooks}
50
+ hooks={hooks}
51
+ id={id}
52
+ inspectPath={inspectPath}
53
+ path={[]}
54
+ />
55
</div>
56
);
57
}
@@ -46,12 +61,16 @@ type InnerHooksTreeViewProps = {|
61
canEditHooks: boolean,
62
hooks: HooksTree,
63
id: number,
64
+ inspectPath: InspectPath,
65
+ path: Array<string | number>,
66
|};
67
68
export function InnerHooksTreeView({
69
canEditHooks,
70
hooks,
71
id,
72
+ inspectPath,
73
+ path,
74
}: InnerHooksTreeViewProps) {
75
// $FlowFixMe "Missing type annotation for U" whatever that means
76
return hooks.map((hook, index) => (
@@ -60,6 +79,8 @@ export function InnerHooksTreeView({
79
canEditHooks={canEditHooks}
80
hook={hooks[index]}
81
id={id}
82
+ inspectPath={inspectPath}
83
+ path={path.concat([index])}
84
/>
85
));
86
}
@@ -68,10 +89,17 @@ type HookViewProps = {|
89
canEditHooks: boolean,
90
hook: HooksNode,
91
id: number,
71
- path?: Array<any>,
92
+ inspectPath: InspectPath,
93
+ path: Array<string | number>,
94
|};
95
74
-function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
96
+function HookView({
97
+ canEditHooks,
98
+ hook,
99
+ id,
100
+ inspectPath,
101
+ path,
102
+}: HookViewProps) {
103
const { name, id: hookID, isStateEditable, subHooks, value } = hook;
104
105
const bridge = useContext(BridgeContext);
@@ -125,6 +153,24 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
153
}
154
155
if (isCustomHook) {
156
+ const subHooksView = Array.isArray(subHooks) ? (
157
+ <InnerHooksTreeView
158
+ canEditHooks={canEditHooks}
159
+ hooks={subHooks}
160
+ id={id}
161
+ inspectPath={inspectPath}
162
+ path={path.concat(['subHooks'])}
163
+ />
164
+ ) : (
165
+ <KeyValue
166
+ depth={1}
167
+ inspectPath={inspectPath}
168
+ name="subHooks"
169
+ path={path.concat(['subHooks'])}
170
+ value={subHooks}
171
+ />
172
+ );
173
+
174
if (isComplexDisplayValue) {
175
return (
176
<div className={styles.Hook}>
@@ -135,12 +181,14 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
181
</span>
182
</div>
183
<div className={styles.Children} hidden={!isOpen}>
138
- <KeyValue depth={1} name="DebugValue" value={value} />
139
- <InnerHooksTreeView
140
- canEditHooks={canEditHooks}
141
- hooks={subHooks}
142
- id={id}
184
+ <KeyValue
185
+ depth={1}
186
+ inspectPath={inspectPath}
187
+ name="DebugValue"
188
+ path={path.concat(['value'])}
189
+ value={value}
190
/>
191
+ {subHooksView}
192
</div>
193
</div>
194
);
@@ -156,11 +204,7 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
204
<span className={styles.Value}>{displayValue}</span>
205
</div>
206
<div className={styles.Children} hidden={!isOpen}>
159
- <InnerHooksTreeView
160
- canEditHooks={canEditHooks}
161
- hooks={subHooks}
162
- id={id}
163
- />
207
+ {subHooksView}
208
</div>
209
</div>
210
);
@@ -169,12 +213,14 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
213
let overrideValueFn = null;
214
// TODO Maybe read editable value from debug hook?
215
if (canEditHooks && isStateEditable) {
172
- overrideValueFn = (path: Array<string | number>, value: any) => {
216
+ overrideValueFn = (absolutePath: Array<string | number>, value: any) => {
217
const rendererID = store.getRendererIDForElement(id);
218
bridge.send('overrideHookState', {
219
id,
220
hookID,
177
- path,
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.
223
+ path: absolutePath.slice(path.length + 1),
224
rendererID,
225
value,
226
});
@@ -186,8 +232,10 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
232
<div className={styles.Hook}>
233
<KeyValue
234
depth={1}
235
+ inspectPath={inspectPath}
236
name={name}
237
overrideValueFn={overrideValueFn}
238
+ path={path.concat(['value'])}
239
value={value}
240
/>
241
</div>
src/devtools/views/Components/KeyValue.js
+2
-2
@@ -18,7 +18,7 @@ type KeyValueProps = {|
18
inspectPath?: InspectPath,
19
name: string,
20
overrideValueFn?: ?OverrideValueFn,
21
- path?: Array<any>,
21
+ path: Array<any>,
22
value: any,
23
|};
24
@@ -31,7 +31,7 @@ export default function KeyValue({
31
hidden,
32
name,
33
overrideValueFn,
34
- path = [],
34
+ path,
35
value,
36
}: KeyValueProps) {
37
const [isOpen, setIsOpen] = useState<boolean>(false);
src/hydration.js
+5
-19
@@ -120,20 +120,6 @@ function createDehydrated(
120
return dehydrated;
121
}
122
123
-function isInspectedPath(
124
- path: Array<string | number> = [],
125
- inspectedPaths: Object
126
-): boolean {
127
- let current = inspectedPaths;
128
- for (let i = 0; i < path.length; i++) {
129
- current = current[path[i]];
130
- if (!current) {
131
- return false;
132
- }
133
- }
134
- return true;
135
-}
136
-
123
/**
124
* Strip out complex data (instances, functions, and data nested > LEVEL_THRESHOLD levels deep).
125
* The paths of the stripped out objects are appended to the `cleaned` list.
@@ -156,7 +142,7 @@ export function dehydrate(
142
data: Object,
143
cleaned: Array<Array<string | number>>,
144
path: Array<string | number>,
159
- inspectedPaths: Object,
145
+ isPathWhitelisted: (path: Array<string | number>) => boolean,
146
level?: number = 0
147
): string | Dehydrated | { [key: string]: string | Dehydrated } {
148
const type = getPropType(data);
@@ -213,7 +199,7 @@ export function dehydrate(
199
};
200
201
case 'array':
216
- const arrayPathCheck = isInspectedPath(path, inspectedPaths);
202
+ const arrayPathCheck = isPathWhitelisted(path);
203
if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
204
return createDehydrated(type, true, data, cleaned, path);
205
}
@@ -222,7 +208,7 @@ export function dehydrate(
208
item,
209
cleaned,
210
path.concat([i]),
225
- inspectedPaths,
211
+ isPathWhitelisted,
212
arrayPathCheck ? 1 : level + 1
213
)
214
);
@@ -240,7 +226,7 @@ export function dehydrate(
226
};
227
228
case 'object':
243
- const objectPathCheck = isInspectedPath(path, inspectedPaths);
229
+ const objectPathCheck = isPathWhitelisted(path);
230
if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
231
return createDehydrated(type, true, data, cleaned, path);
232
} else {
@@ -250,7 +236,7 @@ export function dehydrate(
236
data[name],
237
cleaned,
238
path.concat([name]),
253
- inspectedPaths,
239
+ isPathWhitelisted,
240
objectPathCheck ? 1 : level + 1
241
);
242
}