Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DevTools] Named hooks #19097

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeHandle(undefined, () => null);
Dispatcher.useDebugValue(null);
Dispatcher.useDebugName(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
Dispatcher.useMutableSource(
Expand Down Expand Up @@ -233,6 +234,14 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
});
}

function useDebugName(value: any, formatterFn: ?(value: any) => any) {
hookLog.push({
primitive: 'DebugName',
stackError: new Error(),
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
});
}

function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
const hook = nextHook();
hookLog.push({
Expand Down Expand Up @@ -340,6 +349,7 @@ const Dispatcher: DispatcherType = {
useEffect,
useImperativeHandle,
useDebugValue,
useDebugName,
useLayoutEffect,
useMemo,
useReducer,
Expand Down Expand Up @@ -565,6 +575,8 @@ function buildTree(rootStack, readHookLog): HooksTree {

// Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
processDebugValues(rootChildren, null);
// Associate hook names from useDebugName() with the hooks that were called before them.
processDebugNames(rootChildren);

return rootChildren;
}
Expand Down Expand Up @@ -603,6 +615,30 @@ function processDebugValues(
}
}

// The method attributes the name to the last processed hook, if defined.
function processDebugNames(hooksTree: HooksTree): void {
let lastProcessedHookReference: any = null;

for (let i = 0; i < hooksTree.length; i++) {
const hooksNode = hooksTree[i];
if (hooksNode.name === 'DebugName' && hooksNode.subHooks.length === 0) {
if (i !== 0) {
lastProcessedHookReference = hooksTree[i - 1];
// Do not append names which are identical to default hook names
if (lastProcessedHookReference.name.toLowerCase() !== hooksNode.value) {
lastProcessedHookReference.name =
// $FlowFixMe: Flow doesn't like mixed types
lastProcessedHookReference.name + ', ' + hooksNode.value;
}
}
hooksTree.splice(i, 1);
i--;
} else {
processDebugNames(hooksNode.subHooks);
}
}
}

export function inspectHooks<Props>(
renderFunction: Props => React$Node,
props: Props,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,59 @@ describe('ReactHooksInspection', () => {
]);
});
});

if (__DEV__) {
describe('useDebugName', () => {
it('should append hook name identifier if it is not equal to default hook name', () => {
function Foo(props) {
const [state] = React.useState('hello world');
// when variable name === hook name, useDebugName should not change `name` key
React.useDebugName('state');
const [name] = React.useState('hello world');
React.useDebugName('name');
return (
<div>
{state}
{name}
</div>
);
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'hello world',
subHooks: [],
},
{
isStateEditable: true,
id: 2,
name: __DEV__ ? 'State, name' : 'State',
value: 'hello world',
subHooks: [],
},
]);
});

it('should support an optional formatter function param', () => {
function Foo(props) {
const [data] = React.useState(0);
React.useDebugName('data', value => `name:${value}`);
return <div>{data}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: __DEV__ ? 'State, name:data' : 'State',
subHooks: [],
value: 0,
},
]);
});
});
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,93 @@ describe('ReactHooksInspectionIntegration', () => {
});
});

if (__DEV__) {
describe('useDebugName', () => {
it('should work on nested default hooks', () => {
function useInner() {
const [name1] = React.useState(0);
React.useDebugName('name1');
return name1;
}
function useOuter() {
const name2 = React.useRef(null);
React.useDebugName('name2');
useInner();
return name2;
}
function Example() {
useOuter();
return null;
}
const renderer = ReactTestRenderer.create(<Example />);
const childFiber = renderer.root.findByType(Example)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Outer',
value: undefined,
subHooks: [
{
isStateEditable: false,
id: 0,
name: __DEV__ ? 'Ref, name2' : 'Ref',
value: null,
subHooks: [],
},
{
isStateEditable: false,
id: null,
name: 'Inner',
value: undefined,
subHooks: [
{
isStateEditable: true,
id: 2,
name: __DEV__ ? 'State, name1' : 'State',
value: 0,
subHooks: [],
},
],
},
],
},
]);
});

it('supports more than one name when assigned', () => {
function Foo(props) {
const [name] = React.useState('hello world');
React.useDebugName('data');
React.useDebugName('name');
return <div>{name}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: __DEV__ ? 'State, data, name' : 'State',
value: 'hello world',
subHooks: [],
},
]);
});

it('should ignore useDebugName() if there is no hook that it can handle', () => {
function Example() {
React.useDebugName('this is invalid');
return null;
}
const renderer = ReactTestRenderer.create(<Example />);
const childFiber = renderer.root.findByType(Example)._currentFiber();
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toHaveLength(0);
});
});
}

it('should support defaultProps and lazy', async () => {
const Suspense = React.Suspense;

Expand Down
79 changes: 79 additions & 0 deletions packages/react-devtools-shell/src/app/NamedHooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import * as React from 'react';
import {
createContext,
useState,
useContext,
useRef,
useReducer,
useCallback,
useMemo,
useDebugName,
} from 'react';

function useNestedInnerHook() {
const [nestedState] = useState(123);
useDebugName('nestedState');
return nestedState;
}
function useNestedOuterHook() {
return useNestedInnerHook();
}

const initialData = {foo: 'FOO', bar: 'BAR'};

function reducer(state, action) {
switch (action.type) {
case 'swap':
return {foo: state.bar, bar: state.foo};
default:
throw new Error();
}
}

const StringContext = createContext('123');

export default function NamedHooks(props: any) {
const [count, setCount] = useState(0);
useDebugName('count');
const memoizedSetClick = useCallback(() => setCount(count + 1), [count]);
useDebugName('memoizedSetClick');

const [state, setState] = useState(false); // eslint-disable-line

const [data, dispatch] = useReducer(reducer, initialData); // eslint-disable-line
useDebugName('data');
const memoizedDataDispatcher = useCallback(
() => dispatch({type: 'swap'}),
[],
);
useDebugName('memoizedDataDispatcher');

const memoizedCountMultiplied = useMemo(() => count * 2, [count]);
useDebugName('memoizedCountMultiplied');
const ctxValue = useContext(StringContext);
useDebugName('StringContext');
const spanRef = useRef(null);
useDebugName('spanRef');

useNestedOuterHook();

return (
<>
<h1>Named hooks</h1>
<button onClick={memoizedSetClick}>
Count: {count} {memoizedCountMultiplied}
</button>
<button onClick={memoizedDataDispatcher}>Swap reducer values</button>
<span ref={spanRef}>Context: {ctxValue}</span>
</>
);
}
2 changes: 2 additions & 0 deletions packages/react-devtools-shell/src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ReactNativeWeb from './ReactNativeWeb';
import ToDoList from './ToDoList';
import Toggle from './Toggle';
import SuspenseTree from './SuspenseTree';
import NamedHooks from './NamedHooks';
import {ignoreErrors, ignoreWarnings} from './console';

import './styles.css';
Expand Down Expand Up @@ -59,6 +60,7 @@ function mountTestApp() {
mountHelper(SuspenseTree);
mountHelper(DeeplyNestedComponents);
mountHelper(Iframe);
mountHelper(NamedHooks);
}

function unmountTestApp() {
Expand Down
1 change: 1 addition & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ export const Dispatcher: DispatcherType = {
useEffect: noop,
// Debugging effect
useDebugValue: noop,
useDebugName: noop,
useResponder,
useDeferredValue,
useTransition,
Expand Down
3 changes: 3 additions & 0 deletions packages/react-named-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# react-named-hooks

Provides auto injecting of useDebugName hook to enhance hook information in React DevTools.
12 changes: 12 additions & 0 deletions packages/react-named-hooks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

'use strict';

export {default} from './src/ReactNamedHooksBabelPlugin';
7 changes: 7 additions & 0 deletions packages/react-named-hooks/npm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-named-hooks.production.min.js');
} else {
module.exports = require('./cjs/react-named-hooks.development.js');
}
27 changes: 27 additions & 0 deletions packages/react-named-hooks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "react-named-hooks",
"version": "0.1.0",
"description": "Babel plugin for auto injecting useDebugName hook.",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-named-hooks"
},
"keywords": [
"react"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/facebook/react/issues"
},
"homepage": "https://reactjs.org/",
"files": [
"LICENSE",
"README.md",
"build-info.json",
"index.js",
"cjs/",
"umd/"
]
}
Loading