Added simple hooks support (same as in legacy DevTools for now)
I had to add a couple of comments because Flow was being a mysterious little shit and I got tired of trying to work around it.
Brian Vaughn committed
Feb 5, 2019 at 09:23 UTC
33deb79ce46af1fae5df28358f5f2c38759fa81e
12 files changed
+187
-63
shells/dev/app/InspectableElements/Contexts.js
+2
-2
@@ -88,8 +88,8 @@ class ModernContextType extends Component<any> {
88
}
89
90
function FunctionalContextConsumer() {
91
- const string = useContext(StringContext);
92
- return string;
91
+ useContext(StringContext);
92
+ return null;
93
}
94
95
export default function Contexts() {
shells/dev/app/InspectableElements/CustomHooks.js
new
+79
@@ -0,0 +1,79 @@
1
+// @flow
2
+
3
+import React, {
4
+ forwardRef,
5
+ Fragment,
6
+ memo,
7
+ useCallback,
8
+ // $FlowFixMe Flow doesn't yet know about this hook
9
+ useDebugValue,
10
+ useEffect,
11
+ useState,
12
+} from 'react';
13
+
14
+function useNestedInnerHook() {
15
+ return useState(123);
16
+}
17
+function useNestedOuterHook() {
18
+ return useNestedInnerHook();
19
+}
20
+
21
+function FunctionWithHooks(props: any, ref: React$Ref<any>) {
22
+ const [count, updateCount] = useState(0);
23
+
24
+ // Custom hook with a custom debug label
25
+ const debouncedCount = useDebounce(count, 1000);
26
+
27
+ const onClick = useCallback(
28
+ function onClick() {
29
+ updateCount(count + 1);
30
+ },
31
+ [count]
32
+ );
33
+
34
+ // Tests nested custom hooks
35
+ useNestedOuterHook();
36
+
37
+ return <button onClick={onClick}>Count: {debouncedCount}</button>;
38
+}
39
+const MemoWithHooks = memo(FunctionWithHooks);
40
+const ForwardRefWithHooks = forwardRef(FunctionWithHooks);
41
+
42
+export default function CustomHooks() {
43
+ return (
44
+ <Fragment>
45
+ <FunctionWithHooks />
46
+ <MemoWithHooks />
47
+ <ForwardRefWithHooks />
48
+ </Fragment>
49
+ );
50
+}
51
+
52
+// Below copied from https://usehooks.com/
53
+function useDebounce(value, delay) {
54
+ // State and setters for debounced value
55
+ const [debouncedValue, setDebouncedValue] = useState(value);
56
+
57
+ // Show the value in DevTools
58
+ useDebugValue(debouncedValue);
59
+
60
+ useEffect(
61
+ () => {
62
+ // Update debounced value after delay
63
+ const handler = setTimeout(() => {
64
+ setDebouncedValue(value);
65
+ }, delay);
66
+
67
+ // Cancel the timeout if value changes (also on delay change or unmount)
68
+ // This is how we prevent debounced value from updating if value is changed ...
69
+ // .. within the delay period. Timeout gets cleared and restarted.
70
+ return () => {
71
+ clearTimeout(handler);
72
+ };
73
+ },
74
+ [value, delay] // Only re-call effect if value or delay changes
75
+ );
76
+
77
+ return debouncedValue;
78
+}
79
+// Above copied from https://usehooks.com/
shells/dev/app/InspectableElements/FunctionWithState.js
deleted
-16
@@ -1,16 +0,0 @@
1
-// @flow
2
-
3
-import React, { useCallback, useState } from 'react';
4
-
5
-type Props = {|
6
- initialCount: number,
7
-|};
8
-
9
-export default function FunctionWithState({ initialCount }: Props) {
10
- const [count, setCount] = useState(initialCount);
11
- const handleClick = useCallback(() => {
12
- setCount(count => count + 1);
13
- });
14
-
15
- return <button onClick={handleClick}>Count {count}</button>;
16
-}
shells/dev/app/InspectableElements/InspectableElements.css
new
+5
@@ -0,0 +1,5 @@
1
+.Header {
2
+ font-size: 1.5rem;
3
+ font-weight: bold;
4
+ margin-bottom: 0.5rem;
5
+}
shells/dev/app/InspectableElements/InspectableElements.js
+6
-2
@@ -2,15 +2,19 @@
2
3
import React, { Fragment } from 'react';
4
import Contexts from './Contexts';
5
-import FunctionWithState from './FunctionWithState';
5
+import CustomHooks from './CustomHooks';
6
import NestedProps from './NestedProps';
7
+import styles from './InspectableElements.css';
8
+
9
+// TODO Add Immutable JS example
10
11
export default function InspectableElements() {
12
return (
13
<Fragment>
11
- <FunctionWithState initialCount={1} />
14
+ <div className={styles.Header}>Inspectable elements</div>
15
<NestedProps />
16
<Contexts />
17
+ <CustomHooks />
18
</Fragment>
19
);
20
}
src/backend/renderer.js
+1
-1
@@ -1103,7 +1103,7 @@ export function attach(
1103
canEditValues: false, // TODO
1104
1105
// Inspectable properties.
1106
- // TODO Sanitize props, state, and context
1106
+ // TODO Review sanitization approach for the below inspectable values.
1107
context,
1108
hooks: usesHooks
1109
? cleanForBridge(
src/backend/types.js
-6
@@ -80,9 +80,3 @@ export type HooksNode = {
80
subHooks: Array<HooksNode>,
81
};
82
export type HooksTree = Array<HooksNode>;
83
-
84
-export type InspectedHooks = {|
85
- elementID: string,
86
- id: string,
87
- hooksTree: HooksTree,
88
-|};
src/devtools/views/HooksTree.css
+16
-2
@@ -1,10 +1,24 @@
1
-.HooksTree {
1
+.HooksTreeView {
2
padding: 0.25rem;
3
border-bottom: 1px solid var(--color-base02);
4
}
5
6
-.ComingSoon {
6
+.HooksNode {
7
padding-left: 1rem;
8
+}
9
+
10
+.NameValueRow {
11
+}
12
+
13
+.Name {
14
+ color: var(--color-tree-attr-name);
15
+}
16
+
17
+.Value {
18
+ color: var(--color-tree-attr-value);
19
+}
20
+
21
+.None {
22
color: var(--color-base03);
23
font-style: italic;
24
}
src/devtools/views/HooksTree.js
+52
-11
@@ -1,24 +1,65 @@
1
// @flow
2
3
import React from 'react';
4
+import { getMetaValueLabel } from './utils';
5
import styles from './HooksTree.css';
6
6
-import type { InspectedHooks } from 'src/backend/types';
7
+import type { HooksNode, HooksTree } from 'src/backend/types';
8
8
-type Props = {|
9
- inspectedHooks: InspectedHooks | null,
10
-|};
11
-
12
-export default function HooksTree({ inspectedHooks }: Props) {
13
- if (inspectedHooks === null) {
9
+export function HooksTreeView({ hooksTree }: { hooksTree: HooksTree | null }) {
10
+ if (hooksTree === null) {
11
return null;
12
+ } else {
13
+ return (
14
+ <div className={styles.HooksTreeView}>
15
+ <div className={styles.Item}>hooks</div>
16
+ <InnerHooksTreeView hooksTree={hooksTree} />
17
+ </div>
18
+ );
19
+ }
20
+}
21
+
22
+export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) {
23
+ // $FlowFixMe "Missing type annotation for U" whatever that means
24
+ return hooksTree.map((hooksNode, index) => (
25
+ <HooksNodeView key={index} hooksNode={hooksTree[index]} />
26
+ ));
27
+}
28
+
29
+function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
30
+ const { name, subHooks, value } = hooksNode;
31
+
32
+ // TODO Add click and key handlers for toggling element open/close state.
33
+ // TODO Support editable props
34
+
35
+ const isCustomHook = subHooks.length > 0;
36
+
37
+ // Format data for display to mimic the props/state/context for now.
38
+ const type = typeof value;
39
+ let displayValue;
40
+ if (isCustomHook && value === undefined) {
41
+ displayValue = null;
42
+ } else if (
43
+ type === 'number' ||
44
+ type === 'string' ||
45
+ type === 'boolean' ||
46
+ value == null
47
+ ) {
48
+ displayValue = value;
49
+ } else {
50
+ displayValue = getMetaValueLabel(value);
51
}
52
17
- // TODO
53
return (
19
- <div className={styles.HooksTree}>
20
- hooks
21
- <div className={styles.ComingSoon}>Coming soon...</div>
54
+ <div className={styles.HooksNode}>
55
+ <div className={styles.NameValueRow}>
56
+ <span className={styles.Name}>{name}: </span> {/* $FlowFixMe */}
57
+ <span className={styles.Value}>{displayValue}</span>
58
+ </div>
59
+ <InnerHooksTreeView hooksTree={subHooks} />
60
</div>
61
);
62
}
63
+
64
+// $FlowFixMe
65
+export default React.memo(HooksTreeView);
src/devtools/views/InspectedElementTree.js
+1
-22
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React from 'react';
4
+import { getMetaValueLabel } from './utils';
5
import { meta } from '../../hydration';
6
import styles from './InspectedElementTree.css';
7
@@ -15,7 +16,6 @@ export default function InspectedElementTree({ data, label }: Props) {
16
} else {
17
// TODO Add click and key handlers for toggling element open/close state.
18
// TODO Support editable props
18
-
19
return (
20
<div className={styles.InspectedElementTree}>
21
<div className={styles.Item}>{label}</div>
@@ -108,24 +108,3 @@ function KeyValue({ depth, name, value }: KeyValueProps) {
108
109
return children;
110
}
111
-
112
-function getMetaValueLabel(data: Object): string | null {
113
- switch (data[meta.type]) {
114
- case 'function':
115
- return `${data[meta.name] || 'fn'}()`;
116
- case 'object':
117
- return 'Object';
118
- case 'date':
119
- case 'symbol':
120
- return data[meta.name];
121
- case 'iterator':
122
- return `${data[meta.name]}(…)`;
123
- case 'array_buffer':
124
- case 'data_view':
125
- case 'array':
126
- case 'typed_array':
127
- return `${data[meta.name]}[${data[meta.meta].length}]`;
128
- default:
129
- return null;
130
- }
131
-}
src/devtools/views/SelectedElement.js
+1
-1
@@ -89,7 +89,7 @@ function InspectedElementView({
89
<div className={styles.InspectedElement}>
90
<InspectedElementTree label="props" data={props} />
91
<InspectedElementTree label="state" data={state} />
92
- <HooksTree inspectedHooks={hooks} />
92
+ <HooksTree hooksTree={hooks} />
93
<InspectedElementTree label="context" data={context} />
94
95
{owners !== null && owners.length > 0 && (
src/devtools/views/utils.js
new
+24
@@ -0,0 +1,24 @@
1
+// @flow
2
+
3
+import { meta } from '../../hydration';
4
+
5
+export function getMetaValueLabel(data: Object): string | null {
6
+ switch (data[meta.type]) {
7
+ case 'function':
8
+ return `${data[meta.name] || 'fn'}()`;
9
+ case 'object':
10
+ return 'Object';
11
+ case 'date':
12
+ case 'symbol':
13
+ return data[meta.name];
14
+ case 'iterator':
15
+ return `${data[meta.name]}(…)`;
16
+ case 'array_buffer':
17
+ case 'data_view':
18
+ case 'array':
19
+ case 'typed_array':
20
+ return `${data[meta.name]}[${data[meta.meta].length}]`;
21
+ default:
22
+ return null;
23
+ }
24
+}