@samitouri / QOS-React-2 / commits / 33deb79ce4

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