From 33deb79ce46af1fae5df28358f5f2c38759fa81e Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 5 Feb 2019 09:23:14 +0000 Subject: [PATCH] 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. --- .../dev/app/InspectableElements/Contexts.js | 4 +- .../app/InspectableElements/CustomHooks.js | 79 +++++++++++++++++++ .../InspectableElements/FunctionWithState.js | 16 ---- .../InspectableElements.css | 5 ++ .../InspectableElements.js | 8 +- src/backend/renderer.js | 2 +- src/backend/types.js | 6 -- src/devtools/views/HooksTree.css | 18 ++++- src/devtools/views/HooksTree.js | 63 ++++++++++++--- src/devtools/views/InspectedElementTree.js | 23 +----- src/devtools/views/SelectedElement.js | 2 +- src/devtools/views/utils.js | 24 ++++++ 12 files changed, 187 insertions(+), 63 deletions(-) create mode 100644 shells/dev/app/InspectableElements/CustomHooks.js delete mode 100644 shells/dev/app/InspectableElements/FunctionWithState.js create mode 100644 shells/dev/app/InspectableElements/InspectableElements.css create mode 100644 src/devtools/views/utils.js diff --git a/shells/dev/app/InspectableElements/Contexts.js b/shells/dev/app/InspectableElements/Contexts.js index 2f4515ab40..12362a811e 100644 --- a/shells/dev/app/InspectableElements/Contexts.js +++ b/shells/dev/app/InspectableElements/Contexts.js @@ -88,8 +88,8 @@ class ModernContextType extends Component { } function FunctionalContextConsumer() { - const string = useContext(StringContext); - return string; + useContext(StringContext); + return null; } export default function Contexts() { diff --git a/shells/dev/app/InspectableElements/CustomHooks.js b/shells/dev/app/InspectableElements/CustomHooks.js new file mode 100644 index 0000000000..7e0b318453 --- /dev/null +++ b/shells/dev/app/InspectableElements/CustomHooks.js @@ -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) { + 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 ; +} +const MemoWithHooks = memo(FunctionWithHooks); +const ForwardRefWithHooks = forwardRef(FunctionWithHooks); + +export default function CustomHooks() { + return ( + + + + + + ); +} + +// 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/ diff --git a/shells/dev/app/InspectableElements/FunctionWithState.js b/shells/dev/app/InspectableElements/FunctionWithState.js deleted file mode 100644 index ae445fc246..0000000000 --- a/shells/dev/app/InspectableElements/FunctionWithState.js +++ /dev/null @@ -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 ; -} diff --git a/shells/dev/app/InspectableElements/InspectableElements.css b/shells/dev/app/InspectableElements/InspectableElements.css new file mode 100644 index 0000000000..6d36bfb51d --- /dev/null +++ b/shells/dev/app/InspectableElements/InspectableElements.css @@ -0,0 +1,5 @@ +.Header { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 0.5rem; +} diff --git a/shells/dev/app/InspectableElements/InspectableElements.js b/shells/dev/app/InspectableElements/InspectableElements.js index c93721c55f..fa814cceda 100644 --- a/shells/dev/app/InspectableElements/InspectableElements.js +++ b/shells/dev/app/InspectableElements/InspectableElements.js @@ -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 ( - +
Inspectable elements
+
); } diff --git a/src/backend/renderer.js b/src/backend/renderer.js index ffdc881fd8..468a61a2d3 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -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( diff --git a/src/backend/types.js b/src/backend/types.js index eaf7358539..e5202dc07b 100644 --- a/src/backend/types.js +++ b/src/backend/types.js @@ -80,9 +80,3 @@ export type HooksNode = { subHooks: Array, }; export type HooksTree = Array; - -export type InspectedHooks = {| - elementID: string, - id: string, - hooksTree: HooksTree, -|}; diff --git a/src/devtools/views/HooksTree.css b/src/devtools/views/HooksTree.css index 681b63e000..950ca8eb97 100644 --- a/src/devtools/views/HooksTree.css +++ b/src/devtools/views/HooksTree.css @@ -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; } diff --git a/src/devtools/views/HooksTree.js b/src/devtools/views/HooksTree.js index 11f0414312..f2ec250820 100644 --- a/src/devtools/views/HooksTree.js +++ b/src/devtools/views/HooksTree.js @@ -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 ( +
+
hooks
+ +
+ ); + } +} + +export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) { + // $FlowFixMe "Missing type annotation for U" whatever that means + return hooksTree.map((hooksNode, 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 ( -
- hooks -
Coming soon...
+
+
+ {name}: {/* $FlowFixMe */} + {displayValue} +
+
); } + +// $FlowFixMe +export default React.memo(HooksTreeView); diff --git a/src/devtools/views/InspectedElementTree.js b/src/devtools/views/InspectedElementTree.js index 91487ea31d..4f37bf0bea 100644 --- a/src/devtools/views/InspectedElementTree.js +++ b/src/devtools/views/InspectedElementTree.js @@ -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 (
{label}
@@ -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; - } -} diff --git a/src/devtools/views/SelectedElement.js b/src/devtools/views/SelectedElement.js index 3f1bde9d8d..0264a9107c 100644 --- a/src/devtools/views/SelectedElement.js +++ b/src/devtools/views/SelectedElement.js @@ -89,7 +89,7 @@ function InspectedElementView({
- + {owners !== null && owners.length > 0 && ( diff --git a/src/devtools/views/utils.js b/src/devtools/views/utils.js new file mode 100644 index 0000000000..e5ec47254f --- /dev/null +++ b/src/devtools/views/utils.js @@ -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; + } +}