diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index c9be0d66f7684..cdac7f3bd413c 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -84,6 +84,7 @@ function getPrimitiveStackCache(): Map> { Dispatcher.useEffect(() => {}); Dispatcher.useImperativeHandle(undefined, () => null); Dispatcher.useDebugValue(null); + Dispatcher.useDebugName(null); Dispatcher.useCallback(() => {}); Dispatcher.useMemo(() => null); Dispatcher.useMutableSource( @@ -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(callback: T, inputs: Array | void | null): T { const hook = nextHook(); hookLog.push({ @@ -340,6 +349,7 @@ const Dispatcher: DispatcherType = { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useReducer, @@ -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; } @@ -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( renderFunction: Props => React$Node, props: Props, diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js index 49786ddfd8ac2..1e7f45520763f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js @@ -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 ( +
+ {state} + {name} +
+ ); + } + 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
{data}
; + } + const tree = ReactDebugTools.inspectHooks(Foo, {}); + expect(tree).toEqual([ + { + isStateEditable: true, + id: 0, + name: __DEV__ ? 'State, name:data' : 'State', + subHooks: [], + value: 0, + }, + ]); + }); + }); + } }); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 23503b7c54e06..7aff1d933c61d 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -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(); + 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
{name}
; + } + 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(); + 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; diff --git a/packages/react-devtools-shell/src/app/NamedHooks/index.js b/packages/react-devtools-shell/src/app/NamedHooks/index.js new file mode 100644 index 0000000000000..a4a23836ed76b --- /dev/null +++ b/packages/react-devtools-shell/src/app/NamedHooks/index.js @@ -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 ( + <> +

Named hooks

+ + + Context: {ctxValue} + + ); +} diff --git a/packages/react-devtools-shell/src/app/index.js b/packages/react-devtools-shell/src/app/index.js index 8483be6b050a9..a48004f8971d4 100644 --- a/packages/react-devtools-shell/src/app/index.js +++ b/packages/react-devtools-shell/src/app/index.js @@ -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'; @@ -59,6 +60,7 @@ function mountTestApp() { mountHelper(SuspenseTree); mountHelper(DeeplyNestedComponents); mountHelper(Iframe); + mountHelper(NamedHooks); } function unmountTestApp() { diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index e2180a47542a1..661e37f8c0029 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -523,6 +523,7 @@ export const Dispatcher: DispatcherType = { useEffect: noop, // Debugging effect useDebugValue: noop, + useDebugName: noop, useResponder, useDeferredValue, useTransition, diff --git a/packages/react-named-hooks/README.md b/packages/react-named-hooks/README.md new file mode 100644 index 0000000000000..c029d39e1d16f --- /dev/null +++ b/packages/react-named-hooks/README.md @@ -0,0 +1,3 @@ +# react-named-hooks + +Provides auto injecting of useDebugName hook to enhance hook information in React DevTools. diff --git a/packages/react-named-hooks/index.js b/packages/react-named-hooks/index.js new file mode 100644 index 0000000000000..845c58b1df2ee --- /dev/null +++ b/packages/react-named-hooks/index.js @@ -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'; diff --git a/packages/react-named-hooks/npm/index.js b/packages/react-named-hooks/npm/index.js new file mode 100644 index 0000000000000..4fd85af16ca47 --- /dev/null +++ b/packages/react-named-hooks/npm/index.js @@ -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'); +} diff --git a/packages/react-named-hooks/package.json b/packages/react-named-hooks/package.json new file mode 100644 index 0000000000000..ef589040d1033 --- /dev/null +++ b/packages/react-named-hooks/package.json @@ -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/" + ] +} diff --git a/packages/react-named-hooks/src/ReactNamedHooksBabelPlugin.js b/packages/react-named-hooks/src/ReactNamedHooksBabelPlugin.js new file mode 100644 index 0000000000000..eb2fe4779f16b --- /dev/null +++ b/packages/react-named-hooks/src/ReactNamedHooksBabelPlugin.js @@ -0,0 +1,64 @@ +/** + * 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. + */ + +'use strict'; + +export default function(babel, opts = {}) { + if (typeof babel.getEnv === 'function') { + // Only available in Babel 7. + const env = babel.getEnv(); + if (env !== 'development' && !opts.skipEnvCheck) { + throw new Error( + 'React Named Hooks Babel transform should only be enabled in development environment. ' + + 'Instead, the environment is: "' + + env + + '". If you want to override this check, pass {skipEnvCheck: true} as plugin options.', + ); + } + } + + const {types: t} = babel; + + return { + visitor: { + VariableDeclarator(path) { + const {node} = path; + const hookName = node.init.callee.name.slice(3).toLowerCase(); + + let debugName; + switch (hookName) { + case 'state': + case 'reducer': + debugName = node.id.elements[0].name; + break; + case 'ref': + case 'callback': + case 'memo': + debugName = node.id.name; + break; + case 'context': + debugName = node.init.arguments[0].name; + break; + default: + return; + } + + if (hookName === debugName.toLowerCase()) return; + + if (debugName) { + path.parentPath.insertAfter( + t.expressionStatement( + t.callExpression(t.identifier('useDebugName'), [ + t.stringLiteral(debugName), + ]), + ), + ); + } + }, + }, + }; +} diff --git a/packages/react-named-hooks/src/__tests__/ReactNamedHooksBabelPlugin-test.js b/packages/react-named-hooks/src/__tests__/ReactNamedHooksBabelPlugin-test.js new file mode 100644 index 0000000000000..91b9161626635 --- /dev/null +++ b/packages/react-named-hooks/src/__tests__/ReactNamedHooksBabelPlugin-test.js @@ -0,0 +1,132 @@ +/** + * 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. + */ + +'use strict'; + +const babel = require('@babel/core'); +const {wrap} = require('jest-snapshot-serializer-raw'); +const namedHooks = require('react-named-hooks'); + +function transform(input, options = {}) { + return wrap( + babel.transform(input, { + babelrc: false, + configFile: false, + plugins: [ + '@babel/syntax-jsx', + [ + namedHooks, + { + skipEnvCheck: true, + // To simplify debugging tests: + emitFullSignatures: true, + ...options.freshOptions, + }, + ], + ...(options.plugins || []), + ], + }).code, + ); +} + +describe('ReactNamedHooksBabelPlugin', () => { + it('injects first array item for useState, useReducer', () => { + expect( + transform(` + function Foo() { + const [count, setCount] = useState(0); + + const [data, dispatch] = useReducer(reducer, initialData); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('do not injects when lowercased first array item is a default hook name for useState, useReducer', () => { + expect( + transform(` + function Foo() { + const [State, setCount] = useState(0); + + const [reduceR, dispatch] = useReducer(reducer, initialData); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('inejcts identifier for useRef, useCallback, useMemo', () => { + expect( + transform(` + function Foo() { + const spanRef = useRef(null); + + const memoizedSetClick = useCallback(() => setCount(count + 1), [count]); + + const memoizedCountMultiplied = useMemo(() => count * 2, [count]); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('do not injects when lowercased identifier is a default hook name for useRef, useCallback, useMemo', () => { + expect( + transform(` + function Foo() { + const Ref = useRef(null); + + const CallBack = useCallback(() => setCount(count + 1), [count]); + + const memo = useMemo(() => count * 2, [count]); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('inejcts first argument for useContext', () => { + expect( + transform(` + function Foo() { + const ctxValue = useContext(StringContext); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('do not injects when lowercased first argument is a default hook name for useContext', () => { + expect( + transform(` + function Foo() { + const ctxValue = useContext(Context); + return
; + } + `), + ).toMatchSnapshot(); + }); + + it('injects for default hooks within nested custom hook', () => { + expect( + transform(` + function useNestedInnerHook() { + const [nestedState] = useState(123); + return nestedState; + } + function useNestedOuterHook() { + return useNestedInnerHook(); + } + function Foo() { + useNestedOuterHook(); + return
; + } + `), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/react-named-hooks/src/__tests__/__snapshots__/ReactNamedHooksBabelPlugin-test.js.snap b/packages/react-named-hooks/src/__tests__/__snapshots__/ReactNamedHooksBabelPlugin-test.js.snap new file mode 100644 index 0000000000000..7456d9e0ea92d --- /dev/null +++ b/packages/react-named-hooks/src/__tests__/__snapshots__/ReactNamedHooksBabelPlugin-test.js.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReactNamedHooksBabelPlugin injects first array item for useState, useReducer 1`] = ` +function Foo() { + const [count, setCount] = useState(0); + useDebugName("count"); + const [data, dispatch] = useReducer(reducer, initialData); + useDebugName("data"); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin do not injects when lowercased first array item is a default hook name for useState, useReducer 1`] = ` +function Foo() { + const [State, setCount] = useState(0); + const [reduceR, dispatch] = useReducer(reducer, initialData); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin inejcts identifier for useRef, useCallback, useMemo 1`] = ` +function Foo() { + const spanRef = useRef(null); + useDebugName("spanRef"); + const memoizedSetClick = useCallback(() => setCount(count + 1), [count]); + useDebugName("memoizedSetClick"); + const memoizedCountMultiplied = useMemo(() => count * 2, [count]); + useDebugName("memoizedCountMultiplied"); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin do not injects when lowercased identifier is a default hook name for useRef, useCallback, useMemo 1`] = ` +function Foo() { + const Ref = useRef(null); + const CallBack = useCallback(() => setCount(count + 1), [count]); + const memo = useMemo(() => count * 2, [count]); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin inejcts first argument for useContext 1`] = ` +function Foo() { + const ctxValue = useContext(StringContext); + useDebugName("StringContext"); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin do not injects when lowercased first argument is a default hook name for useContext 1`] = ` +function Foo() { + const ctxValue = useContext(Context); + return
; +} +`; + +exports[`ReactNamedHooksBabelPlugin injects for default hooks within nested custom hook 1`] = ` +function useNestedInnerHook() { + const [nestedState] = useState(123); + useDebugName("nestedState"); + return nestedState; +} + +function useNestedOuterHook() { + return useNestedInnerHook(); +} + +function Foo() { + useNestedOuterHook(); + return
; +} +`; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 7c24cfa2b3b90..e6258c8d31ec3 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -117,6 +117,7 @@ export type HookType = | 'useMemo' | 'useImperativeHandle' | 'useDebugValue' + | 'useDebugName' | 'useResponder' | 'useDeferredValue' | 'useTransition' @@ -1390,6 +1391,12 @@ function mountDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { const updateDebugValue = mountDebugValue; +function mountDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + // no-op as mountDebugValue +} + +const updateDebugName = mountDebugName; + function mountCallback(callback: T, deps: Array | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; @@ -1752,6 +1759,7 @@ export const ContextOnlyDispatcher: Dispatcher = { useRef: throwInvalidHookError, useState: throwInvalidHookError, useDebugValue: throwInvalidHookError, + useDebugName: throwInvalidHookError, useResponder: throwInvalidHookError, useDeferredValue: throwInvalidHookError, useTransition: throwInvalidHookError, @@ -1774,6 +1782,7 @@ const HooksDispatcherOnMount: Dispatcher = { useRef: mountRef, useState: mountState, useDebugValue: mountDebugValue, + useDebugName: mountDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: mountDeferredValue, useTransition: mountTransition, @@ -1796,6 +1805,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { useRef: updateRef, useState: updateState, useDebugValue: updateDebugValue, + useDebugName: updateDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: updateDeferredValue, useTransition: updateTransition, @@ -1818,6 +1828,7 @@ const HooksDispatcherOnRerender: Dispatcher = { useRef: updateRef, useState: rerenderState, useDebugValue: updateDebugValue, + useDebugName: updateDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: rerenderDeferredValue, useTransition: rerenderTransition, @@ -1953,6 +1964,11 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + mountHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2085,6 +2101,11 @@ if (__DEV__) { updateHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + updateHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2217,6 +2238,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2350,6 +2376,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2493,6 +2524,12 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2641,6 +2678,12 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2790,6 +2833,12 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index fbb9a55b27918..894cb9828dbaa 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -118,6 +118,7 @@ export type HookType = | 'useMemo' | 'useImperativeHandle' | 'useDebugValue' + | 'useDebugName' | 'useResponder' | 'useDeferredValue' | 'useTransition' @@ -1389,6 +1390,12 @@ function mountDebugValue(value: T, formatterFn: ?(value: T) => mixed): void { const updateDebugValue = mountDebugValue; +function mountDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + // no-op as mountDebugValue +} + +const updateDebugName = mountDebugName; + function mountCallback(callback: T, deps: Array | void | null): T { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; @@ -1773,6 +1780,7 @@ export const ContextOnlyDispatcher: Dispatcher = { useRef: throwInvalidHookError, useState: throwInvalidHookError, useDebugValue: throwInvalidHookError, + useDebugName: throwInvalidHookError, useResponder: throwInvalidHookError, useDeferredValue: throwInvalidHookError, useTransition: throwInvalidHookError, @@ -1795,6 +1803,7 @@ const HooksDispatcherOnMount: Dispatcher = { useRef: mountRef, useState: mountState, useDebugValue: mountDebugValue, + useDebugName: mountDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: mountDeferredValue, useTransition: mountTransition, @@ -1817,6 +1826,7 @@ const HooksDispatcherOnUpdate: Dispatcher = { useRef: updateRef, useState: updateState, useDebugValue: updateDebugValue, + useDebugName: updateDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: updateDeferredValue, useTransition: updateTransition, @@ -1839,6 +1849,7 @@ const HooksDispatcherOnRerender: Dispatcher = { useRef: updateRef, useState: rerenderState, useDebugValue: updateDebugValue, + useDebugName: updateDebugName, useResponder: createDeprecatedResponderListener, useDeferredValue: rerenderDeferredValue, useTransition: rerenderTransition, @@ -1974,6 +1985,11 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + mountHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2106,6 +2122,11 @@ if (__DEV__) { updateHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + mountHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2238,6 +2259,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2371,6 +2397,11 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2514,6 +2545,12 @@ if (__DEV__) { mountHookTypesDev(); return mountDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + mountHookTypesDev(); + return mountDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2662,6 +2699,12 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, @@ -2811,6 +2854,12 @@ if (__DEV__) { updateHookTypesDev(); return updateDebugValue(value, formatterFn); }, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void { + currentHookNameInDev = 'useDebugName'; + warnInvalidHookAccess(); + updateHookTypesDev(); + return updateDebugName(name, formatterFn); + }, useResponder( responder: ReactEventResponder, props, diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index d97c5432058e4..724bf572e8849 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -342,6 +342,7 @@ export type Dispatcher = {| deps: Array | void | null, ): void, useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void, + useDebugName(name: T, formatterFn: ?(name: T) => mixed): void, useResponder( responder: ReactEventResponder, props: Object, diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 0345b0ebada00..1bb8db2ba1616 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -21,6 +21,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useReducer, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index 7056502425758..1963053e30e98 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -21,6 +21,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useReducer, diff --git a/packages/react/index.js b/packages/react/index.js index c30ed85e69a9d..fb7031420a590 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -50,6 +50,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useReducer, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 51e499d2cc3b4..d62dad187909e 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -21,6 +21,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useMutableSource, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 67a221ca674b7..2f322f6255140 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -21,6 +21,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useReducer, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 828415cc67025..e1cb989f5e4e3 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -38,6 +38,7 @@ import { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useMutableSource, @@ -89,6 +90,7 @@ export { useEffect, useImperativeHandle, useDebugValue, + useDebugName, useLayoutEffect, useMemo, useMutableSource, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index acc47e1d6b9f8..56aefa1e6c180 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -152,6 +152,16 @@ export function useDebugValue( } } +export function useDebugName( + name: T, + formatterFn: ?(name: T) => mixed, +): void { + if (__DEV__) { + const dispatcher = resolveDispatcher(); + return dispatcher.useDebugName(name, formatterFn); + } +} + export const emptyObject = {}; export function useResponder( diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 1c319ce45f3f5..547d58a077e3d 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -667,6 +667,15 @@ const bundles = [ externals: [], }, + /******* React Named Hooks *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-named-hooks', + global: 'ReactNamedHooks', + externals: [], + }, + /******* React Fresh *******/ { bundleTypes: [NODE_DEV, NODE_PROD],