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

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 }