From 5bf400ed7447a63123e507a27068124ddb436263 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 9 Jan 2019 21:13:52 -0800 Subject: [PATCH] Support custom values for custom hooks --- .../react-debug-tools/src/ReactDebugHooks.js | 34 ++++- .../ReactHooksInspection-test.internal.js | 3 +- ...ooksInspectionIntegration-test.internal.js | 116 ++++++++++++++++++ .../src/ReactFiberDispatcher.js | 2 + .../react-reconciler/src/ReactFiberHooks.js | 6 + packages/react/src/React.js | 2 + packages/react/src/ReactHooks.js | 5 + 7 files changed, 166 insertions(+), 2 deletions(-) diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 2a426bc4e44d2..ea73a480c3fb4 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -55,6 +55,7 @@ function getPrimitiveStackCache(): Map> { Dispatcher.useLayoutEffect(() => {}); Dispatcher.useEffect(() => {}); Dispatcher.useImperativeMethods(undefined, () => null); + Dispatcher.useDebugValueLabel(null); Dispatcher.useCallback(() => {}); Dispatcher.useMemo(() => null); } finally { @@ -180,6 +181,14 @@ function useImperativeMethods( }); } +function useDebugValueLabel(valueLabel: any) { + hookLog.push({ + primitive: 'DebugValueLabel', + stackError: new Error(), + value: valueLabel, + }); +} + function useCallback(callback: T, inputs: Array | void | null): T { let hook = nextHook(); hookLog.push({ @@ -206,6 +215,7 @@ const Dispatcher = { useContext, useEffect, useImperativeMethods, + useDebugValueLabel, useLayoutEffect, useMemo, useReducer, @@ -388,7 +398,7 @@ function buildTree(rootStack, readHookLog): HooksTree { let children = []; levelChildren.push({ name: parseCustomHookName(stack[j - 1].functionName), - value: undefined, // TODO: Support custom inspectable values. + value: undefined, subHooks: children, }); stackOfChildren.push(levelChildren); @@ -402,9 +412,31 @@ function buildTree(rootStack, readHookLog): HooksTree { subHooks: [], }); } + + // Associate custom hook values (useInpect() hook entries) with the correct hooks + rootChildren.forEach(hooksNode => rollupDebugValueLabels(hooksNode)); + return rootChildren; } +function rollupDebugValueLabels(hooksNode: HooksNode): void { + let useInpectHooksNodes: Array = []; + hooksNode.subHooks = hooksNode.subHooks.filter(subHooksNode => { + if (subHooksNode.name === 'DebugValueLabel') { + useInpectHooksNodes.push(subHooksNode); + return false; + } else { + rollupDebugValueLabels(subHooksNode); + return true; + } + }); + if (useInpectHooksNodes.length === 1) { + hooksNode.value = useInpectHooksNodes[0].value; + } else if (useInpectHooksNodes.length > 1) { + hooksNode.value = useInpectHooksNodes.map(({value}) => value); + } +} + export function inspectHooks( currentDispatcher: CurrentDispatcherRef, renderFunction: Props => React$Node, diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js index 4611bd7d3fb8b..10f83329cfd26 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js @@ -46,6 +46,7 @@ describe('ReactHooksInspection', () => { it('should inspect a simple custom hook', () => { function useCustom(value) { let [state] = React.useState(value); + React.useDebugValueLabel('custom hook label'); return state; } function Foo(props) { @@ -56,7 +57,7 @@ describe('ReactHooksInspection', () => { expect(tree).toEqual([ { name: 'Custom', - value: undefined, + value: 'custom hook label', subHooks: [ { name: 'State', diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js index 80ed37640c316..a470cfea34977 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.internal.js @@ -235,6 +235,122 @@ describe('ReactHooksInspectionIntergration', () => { ]); }); + describe('useDebugValueLabel', () => { + it('should support inspectable values for multiple custom hooks', () => { + function useLabeledValue(label) { + let [value] = React.useState(label); + React.useDebugValueLabel(`custom label ${label}`); + return value; + } + function useAnonymous(label) { + let [value] = React.useState(label); + return value; + } + function Example(props) { + useLabeledValue('a'); + React.useState('b'); + useAnonymous('c'); + useLabeledValue('d'); + return null; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Example)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber( + currentDispatcher, + childFiber, + ); + expect(tree).toEqual([ + { + name: 'LabeledValue', + value: 'custom label a', + subHooks: [{name: 'State', value: 'a', subHooks: []}], + }, + { + name: 'State', + value: 'b', + subHooks: [], + }, + { + name: 'Anonymous', + value: undefined, + subHooks: [{name: 'State', value: 'c', subHooks: []}], + }, + { + name: 'LabeledValue', + value: 'custom label d', + subHooks: [{name: 'State', value: 'd', subHooks: []}], + }, + ]); + }); + + it('should support inspectable values for nested custom hooks', () => { + function useInner() { + React.useDebugValueLabel('inner'); + } + function useOuter() { + React.useDebugValueLabel('outer'); + useInner(); + } + function Example(props) { + useOuter(); + return null; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Example)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber( + currentDispatcher, + childFiber, + ); + expect(tree).toEqual([ + { + name: 'Outer', + value: 'outer', + subHooks: [{name: 'Inner', value: 'inner', subHooks: []}], + }, + ]); + }); + + it('should support multiple inspectable values per custom hooks', () => { + function useMultiLabelCustom() { + React.useDebugValueLabel('one'); + React.useDebugValueLabel('two'); + React.useDebugValueLabel('three'); + } + function useSingleLabelCustom(value) { + React.useDebugValueLabel(`single ${value}`); + } + function Example(props) { + useSingleLabelCustom('one'); + useMultiLabelCustom(); + useSingleLabelCustom('two'); + return null; + } + let renderer = ReactTestRenderer.create(); + let childFiber = renderer.root.findByType(Example)._currentFiber(); + let tree = ReactDebugTools.inspectHooksOfFiber( + currentDispatcher, + childFiber, + ); + expect(tree).toEqual([ + { + name: 'SingleLabelCustom', + value: 'single one', + subHooks: [], + }, + { + name: 'MultiLabelCustom', + value: ['one', 'two', 'three'], + subHooks: [], + }, + { + name: 'SingleLabelCustom', + value: 'single two', + subHooks: [], + }, + ]); + }); + }); + it('should support defaultProps and lazy', async () => { let Suspense = React.Suspense; diff --git a/packages/react-reconciler/src/ReactFiberDispatcher.js b/packages/react-reconciler/src/ReactFiberDispatcher.js index f86319e3946fa..80965c9285642 100644 --- a/packages/react-reconciler/src/ReactFiberDispatcher.js +++ b/packages/react-reconciler/src/ReactFiberDispatcher.js @@ -13,6 +13,7 @@ import { useContext, useEffect, useImperativeMethods, + useDebugValueLabel, useLayoutEffect, useMemo, useReducer, @@ -26,6 +27,7 @@ export const Dispatcher = { useContext, useEffect, useImperativeMethods, + useDebugValueLabel, useLayoutEffect, useMemo, useReducer, diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 5122a8dda4a0e..878a38dc76e1d 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -588,6 +588,12 @@ export function useImperativeMethods( }, nextInputs); } +export function useDebugValueLabel(valueLabel: string): void { + // This hook is normally a no-op. + // The react-debug-hooks package injects its own implementation + // so that e.g. DevTools can display customhook values. +} + export function useCallback( callback: T, inputs: Array | void | null, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index c19a6653e31bf..17241d7e2c07a 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -33,6 +33,7 @@ import { useContext, useEffect, useImperativeMethods, + useDebugValueLabel, useLayoutEffect, useMemo, useReducer, @@ -99,6 +100,7 @@ if (enableHooks) { React.useContext = useContext; React.useEffect = useEffect; React.useImperativeMethods = useImperativeMethods; + React.useDebugValueLabel = useDebugValueLabel; React.useLayoutEffect = useLayoutEffect; React.useMemo = useMemo; React.useReducer = useReducer; diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 1e27f711087c2..599a15fd26780 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -110,3 +110,8 @@ export function useImperativeMethods( const dispatcher = resolveDispatcher(); return dispatcher.useImperativeMethods(ref, create, inputs); } + +export function useDebugValueLabel(valueLabel: string) { + const dispatcher = resolveDispatcher(); + return dispatcher.useDebugValueLabel(valueLabel); +}