Skip to content

Commit

Permalink
Support custom values for custom hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Vaughn committed Jan 10, 2019
1 parent ce0f030 commit 5bf400e
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 2 deletions.
34 changes: 33 additions & 1 deletion packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeMethods(undefined, () => null);
Dispatcher.useDebugValueLabel(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
} finally {
Expand Down Expand Up @@ -180,6 +181,14 @@ function useImperativeMethods<T>(
});
}

function useDebugValueLabel(valueLabel: any) {
hookLog.push({
primitive: 'DebugValueLabel',
stackError: new Error(),
value: valueLabel,
});
}

function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
let hook = nextHook();
hookLog.push({
Expand All @@ -206,6 +215,7 @@ const Dispatcher = {
useContext,
useEffect,
useImperativeMethods,
useDebugValueLabel,
useLayoutEffect,
useMemo,
useReducer,
Expand Down Expand Up @@ -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);
Expand All @@ -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> = [];
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<Props>(
currentDispatcher: CurrentDispatcherRef,
renderFunction: Props => React$Node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -56,7 +57,7 @@ describe('ReactHooksInspection', () => {
expect(tree).toEqual([
{
name: 'Custom',
value: undefined,
value: 'custom hook label',
subHooks: [
{
name: 'State',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Example />);
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(<Example />);
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(<Example />);
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;

Expand Down
2 changes: 2 additions & 0 deletions packages/react-reconciler/src/ReactFiberDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useContext,
useEffect,
useImperativeMethods,
useDebugValueLabel,
useLayoutEffect,
useMemo,
useReducer,
Expand All @@ -26,6 +27,7 @@ export const Dispatcher = {
useContext,
useEffect,
useImperativeMethods,
useDebugValueLabel,
useLayoutEffect,
useMemo,
useReducer,
Expand Down
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,12 @@ export function useImperativeMethods<T>(
}, 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<T>(
callback: T,
inputs: Array<mixed> | void | null,
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/React.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
useContext,
useEffect,
useImperativeMethods,
useDebugValueLabel,
useLayoutEffect,
useMemo,
useReducer,
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/ReactHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,8 @@ export function useImperativeMethods<T>(
const dispatcher = resolveDispatcher();
return dispatcher.useImperativeMethods(ref, create, inputs);
}

export function useDebugValueLabel(valueLabel: string) {
const dispatcher = resolveDispatcher();
return dispatcher.useDebugValueLabel(valueLabel);
}

0 comments on commit 5bf400e

Please sign in to comment.