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.
This commit is contained in:
Brian Vaughn
2019-02-05 09:23:14 +00:00
parent d0d5b677de
commit 33deb79ce4
12 changed files with 187 additions and 63 deletions
@@ -88,8 +88,8 @@ class ModernContextType extends Component<any> {
}
function FunctionalContextConsumer() {
const string = useContext(StringContext);
return string;
useContext(StringContext);
return null;
}
export default function Contexts() {
@@ -0,0 +1,79 @@
// @flow
import React, {
forwardRef,
Fragment,
memo,
useCallback,
// $FlowFixMe Flow doesn't yet know about this hook
useDebugValue,
useEffect,
useState,
} from 'react';
function useNestedInnerHook() {
return useState(123);
}
function useNestedOuterHook() {
return useNestedInnerHook();
}
function FunctionWithHooks(props: any, ref: React$Ref<any>) {
const [count, updateCount] = useState(0);
// Custom hook with a custom debug label
const debouncedCount = useDebounce(count, 1000);
const onClick = useCallback(
function onClick() {
updateCount(count + 1);
},
[count]
);
// Tests nested custom hooks
useNestedOuterHook();
return <button onClick={onClick}>Count: {debouncedCount}</button>;
}
const MemoWithHooks = memo(FunctionWithHooks);
const ForwardRefWithHooks = forwardRef(FunctionWithHooks);
export default function CustomHooks() {
return (
<Fragment>
<FunctionWithHooks />
<MemoWithHooks />
<ForwardRefWithHooks />
</Fragment>
);
}
// Below copied from https://usehooks.com/
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
// Show the value in DevTools
useDebugValue(debouncedValue);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
// Above copied from https://usehooks.com/
@@ -1,16 +0,0 @@
// @flow
import React, { useCallback, useState } from 'react';
type Props = {|
initialCount: number,
|};
export default function FunctionWithState({ initialCount }: Props) {
const [count, setCount] = useState(initialCount);
const handleClick = useCallback(() => {
setCount(count => count + 1);
});
return <button onClick={handleClick}>Count {count}</button>;
}
@@ -0,0 +1,5 @@
.Header {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
@@ -2,15 +2,19 @@
import React, { Fragment } from 'react';
import Contexts from './Contexts';
import FunctionWithState from './FunctionWithState';
import CustomHooks from './CustomHooks';
import NestedProps from './NestedProps';
import styles from './InspectableElements.css';
// TODO Add Immutable JS example
export default function InspectableElements() {
return (
<Fragment>
<FunctionWithState initialCount={1} />
<div className={styles.Header}>Inspectable elements</div>
<NestedProps />
<Contexts />
<CustomHooks />
</Fragment>
);
}
+1 -1
View File
@@ -1103,7 +1103,7 @@ export function attach(
canEditValues: false, // TODO
// Inspectable properties.
// TODO Sanitize props, state, and context
// TODO Review sanitization approach for the below inspectable values.
context,
hooks: usesHooks
? cleanForBridge(
-6
View File
@@ -80,9 +80,3 @@ export type HooksNode = {
subHooks: Array<HooksNode>,
};
export type HooksTree = Array<HooksNode>;
export type InspectedHooks = {|
elementID: string,
id: string,
hooksTree: HooksTree,
|};
+16 -2
View File
@@ -1,10 +1,24 @@
.HooksTree {
.HooksTreeView {
padding: 0.25rem;
border-bottom: 1px solid var(--color-base02);
}
.ComingSoon {
.HooksNode {
padding-left: 1rem;
}
.NameValueRow {
}
.Name {
color: var(--color-tree-attr-name);
}
.Value {
color: var(--color-tree-attr-value);
}
.None {
color: var(--color-base03);
font-style: italic;
}
+52 -11
View File
@@ -1,24 +1,65 @@
// @flow
import React from 'react';
import { getMetaValueLabel } from './utils';
import styles from './HooksTree.css';
import type { InspectedHooks } from 'src/backend/types';
import type { HooksNode, HooksTree } from 'src/backend/types';
type Props = {|
inspectedHooks: InspectedHooks | null,
|};
export default function HooksTree({ inspectedHooks }: Props) {
if (inspectedHooks === null) {
export function HooksTreeView({ hooksTree }: { hooksTree: HooksTree | null }) {
if (hooksTree === null) {
return null;
} else {
return (
<div className={styles.HooksTreeView}>
<div className={styles.Item}>hooks</div>
<InnerHooksTreeView hooksTree={hooksTree} />
</div>
);
}
}
export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) {
// $FlowFixMe "Missing type annotation for U" whatever that means
return hooksTree.map((hooksNode, index) => (
<HooksNodeView key={index} hooksNode={hooksTree[index]} />
));
}
function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
const { name, subHooks, value } = hooksNode;
// TODO Add click and key handlers for toggling element open/close state.
// TODO Support editable props
const isCustomHook = subHooks.length > 0;
// Format data for display to mimic the props/state/context for now.
const type = typeof value;
let displayValue;
if (isCustomHook && value === undefined) {
displayValue = null;
} else if (
type === 'number' ||
type === 'string' ||
type === 'boolean' ||
value == null
) {
displayValue = value;
} else {
displayValue = getMetaValueLabel(value);
}
// TODO
return (
<div className={styles.HooksTree}>
hooks
<div className={styles.ComingSoon}>Coming soon...</div>
<div className={styles.HooksNode}>
<div className={styles.NameValueRow}>
<span className={styles.Name}>{name}: </span> {/* $FlowFixMe */}
<span className={styles.Value}>{displayValue}</span>
</div>
<InnerHooksTreeView hooksTree={subHooks} />
</div>
);
}
// $FlowFixMe
export default React.memo(HooksTreeView);
+1 -22
View File
@@ -1,6 +1,7 @@
// @flow
import React from 'react';
import { getMetaValueLabel } from './utils';
import { meta } from '../../hydration';
import styles from './InspectedElementTree.css';
@@ -15,7 +16,6 @@ export default function InspectedElementTree({ data, label }: Props) {
} else {
// TODO Add click and key handlers for toggling element open/close state.
// TODO Support editable props
return (
<div className={styles.InspectedElementTree}>
<div className={styles.Item}>{label}</div>
@@ -108,24 +108,3 @@ function KeyValue({ depth, name, value }: KeyValueProps) {
return children;
}
function getMetaValueLabel(data: Object): string | null {
switch (data[meta.type]) {
case 'function':
return `${data[meta.name] || 'fn'}()`;
case 'object':
return 'Object';
case 'date':
case 'symbol':
return data[meta.name];
case 'iterator':
return `${data[meta.name]}(…)`;
case 'array_buffer':
case 'data_view':
case 'array':
case 'typed_array':
return `${data[meta.name]}[${data[meta.meta].length}]`;
default:
return null;
}
}
+1 -1
View File
@@ -89,7 +89,7 @@ function InspectedElementView({
<div className={styles.InspectedElement}>
<InspectedElementTree label="props" data={props} />
<InspectedElementTree label="state" data={state} />
<HooksTree inspectedHooks={hooks} />
<HooksTree hooksTree={hooks} />
<InspectedElementTree label="context" data={context} />
{owners !== null && owners.length > 0 && (
+24
View File
@@ -0,0 +1,24 @@
// @flow
import { meta } from '../../hydration';
export function getMetaValueLabel(data: Object): string | null {
switch (data[meta.type]) {
case 'function':
return `${data[meta.name] || 'fn'}()`;
case 'object':
return 'Object';
case 'date':
case 'symbol':
return data[meta.name];
case 'iterator':
return `${data[meta.name]}(…)`;
case 'array_buffer':
case 'data_view':
case 'array':
case 'typed_array':
return `${data[meta.name]}[${data[meta.meta].length}]`;
default:
return null;
}
}