Repository: facebook/react
Pull request: DevTools: Don't load source files contaning only unnamed hooks

Filter these out earlier and avoid the extra processing.

Resolves #21834

Changed files:
filename: packages/react-devtools-extensions/src/__tests__/__source__/__untransformed__/ComponentWithExternalUseEffect.js
patch: @@ -0,0 +1,19 @@
+/**
+ * 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
+ */
+
+const {useState} = require('react');
+const {useCustom} = require('./useCustom');
+
+function Component(props) {
+  const [count] = useState(0);
+  useCustom();
+  return count;
+}
+
+module.exports = {Component};
\ No newline at end of file

filename: packages/react-devtools-extensions/src/__tests__/__source__/__untransformed__/useCustom.js
patch: @@ -0,0 +1,18 @@
+/**
+ * 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
+ */
+
+const {useEffect} = require('react');
+
+function useCustom() {
+  useEffect(() => {
+    // ...
+  }, []);
+}
+
+module.exports = {useCustom};
\ No newline at end of file

filename: packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js
patch: @@ -104,13 +104,35 @@ describe('parseHookNames', () => {
     expectHookNamesToEqual(hookNames, ['foo', 'bar', 'baz']);
   });
 
-  it('should return null for hooks without names like useEffect', async () => {
+  it('should skip loading source files for unnamed hooks like useEffect', async () => {
     const Component = require('./__source__/__untransformed__/ComponentWithUseEffect')
       .Component;
+
+    // Since this component contains only unnamed hooks, the source code should not even be loaded.
+    fetchMock.mockIf(/.+$/, request => {
+      throw Error(`Unexpected file request for "${request.url}"`);
+    });
+
     const hookNames = await getHookNamesForComponent(Component);
     expectHookNamesToEqual(hookNames, []); // No hooks with names
   });
 
+  it('should skip loading source files for unnamed hooks like useEffect (alternate)', async () => {
+    const Component = require('./__source__/__untransformed__/ComponentWithExternalUseEffect')
+      .Component;
+
+    fetchMock.mockIf(/.+$/, request => {
+      // Since th custom hook contains only unnamed hooks, the source code should not be loaded.
+      if (request.url.endsWith('useCustom.js')) {
+        throw Error(`Unexpected file request for "${request.url}"`);
+      }
+      return Promise.resolve(requireText(request.url, 'utf8'));
+    });
+
+    const hookNames = await getHookNamesForComponent(Component);
+    expectHookNamesToEqual(hookNames, ['count', null]); // No hooks with names
+  });
+
   it('should parse names for custom hooks', async () => {
     const Component = require('./__source__/__untransformed__/ComponentWithNamedCustomHooks')
       .Component;
@@ -239,10 +261,10 @@ describe('parseHookNames', () => {
       await test(
         './__source__/__compiled__/external/ComponentWithMultipleHooksPerLine',
       ); // external source map
-      await test(
-        './__source__/__compiled__/bundle',
-        'ComponentWithMultipleHooksPerLine',
-      ); // bundle source map
+      // await test(
+      //   './__source__/__compiled__/bundle',
+      //   'ComponentWithMultipleHooksPerLine',
+      // ); // bundle source map
     });
 
     // TODO Inline require (e.g. require("react").useState()) isn't supported yet.

filename: packages/react-devtools-extensions/src/astUtils.js
patch: @@ -292,13 +292,6 @@ function isHookName(name: string): boolean {
   return /^use[A-Z0-9].*$/.test(name);
 }
 
-// Determines whether incoming hook is a primitive hook that gets assigned to variables.
-export function isNonDeclarativePrimitiveHook(hook: HooksNode) {
-  return ['Effect', 'ImperativeHandle', 'LayoutEffect', 'DebugValue'].includes(
-    hook.name,
-  );
-}
-
 // Check if the AST Node COULD be a React Hook
 function isPotentialHookDeclaration(path: NodePath): boolean {
   // The array potentialHooksFound will contain all potential hook declaration cases we support

filename: packages/react-devtools-extensions/src/parseHookNames.js
patch: @@ -13,7 +13,7 @@ import {parse} from '@babel/parser';
 import {enableHookNameParsing} from 'react-devtools-feature-flags';
 import LRU from 'lru-cache';
 import {SourceMapConsumer} from 'source-map';
-import {getHookName, isNonDeclarativePrimitiveHook} from './astUtils';
+import {getHookName} from './astUtils';
 import {areSourceMapsAppliedToErrors} from './ErrorTester';
 import {__DEBUG__} from 'react-devtools-shared/src/constants';
 import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
@@ -345,17 +345,6 @@ function findHookNames(
   const map: HookNames = new Map();
 
   hooksList.map(hook => {
-    // TODO (named hooks) We should probably filter before this point,
-    // otherwise we are loading and parsing source maps and ASTs for nothing.
-    if (isNonDeclarativePrimitiveHook(hook)) {
-      if (__DEBUG__) {
-        console.log('findHookNames() Non declarative primitive hook');
-      }
-
-      // Not all hooks have names (e.g. useEffect or useLayoutEffect)
-      return null;
-    }
-
     // We already guard against a null HookSource in parseHookNames()
     const hookSource = ((hook.hookSource: any): HookSource);
     const fileName = hookSource.fileName;
@@ -570,13 +559,29 @@ function flattenHooksList(
 ): void {
   for (let i = 0; i < hooksTree.length; i++) {
     const hook = hooksTree[i];
+
+    if (isUnnamedBuiltInHook(hook)) {
+      // No need to load source code or do any parsing for unnamed hooks.
+      if (__DEBUG__) {
+        console.log('flattenHooksList() Skipping unnamed hook', hook);
+      }
+      continue;
+    }
+
     hooksList.push(hook);
     if (hook.subHooks.length > 0) {
       flattenHooksList(hook.subHooks, hooksList);
     }
   }
 }
 
+// Determines whether incoming hook is a primitive hook that gets assigned to variables.
+function isUnnamedBuiltInHook(hook: HooksNode) {
+  return ['Effect', 'ImperativeHandle', 'LayoutEffect', 'DebugValue'].includes(
+    hook.name,
+  );
+}
+
 function updateLruCache(
   locationKeyToHookSourceData: Map<string, HookSourceData>,
 ): Promise<*> {