diff --git a/packages/react-refresh/src/ReactFreshBabelPlugin.js b/packages/react-refresh/src/ReactFreshBabelPlugin.js
index 9592a994399e5..1d47571bbd57a 100644
--- a/packages/react-refresh/src/ReactFreshBabelPlugin.js
+++ b/packages/react-refresh/src/ReactFreshBabelPlugin.js
@@ -98,9 +98,20 @@ export default function (babel, opts = {}) {
if (!foundInside) {
return false;
}
- // const Foo = hoc1(hoc2(() => {}))
- // export default memo(React.forwardRef(function() {}))
- callback(inferredName, node, path);
+
+ // TODO: this is a hack, we should find a better way to detect this.
+ if (
+ firstArgPath.node.type === 'Identifier' &&
+ inferredName === '%default%'
+ ) {
+ // export default memo(function() {}))
+ // export default forwardRef(function() {}))
+ callback(innerName, node, path);
+ } else {
+ // const Foo = hoc1(hoc2(() => {}))
+ // export default memo(React.forwardRef(function() {}))
+ callback(inferredName, node, path);
+ }
return true;
}
default: {
diff --git a/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js b/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js
index 1f397de0a2466..fc0cdab19ca18 100644
--- a/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js
+++ b/packages/react-refresh/src/__tests__/ReactFreshBabelPlugin-test.js
@@ -206,6 +206,44 @@ describe('ReactFreshBabelPlugin', () => {
).toMatchSnapshot();
});
+ it('registers memo default export', () => {
+ expect(
+ transform(`
+ function Component() {}
+ export default React.memo(Component);
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers forwardRef default export', () => {
+ expect(
+ transform(`
+ function Component() {}
+ export default React.forwardRef(Component);
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers memo default export', () => {
+ expect(
+ transform(`
+ import {memo} from 'react';
+ function Component() {}
+ export default memo(Component);
+ `),
+ ).toMatchSnapshot();
+ });
+
+ it('registers forwardRef default export', () => {
+ expect(
+ transform(`
+ import {forwardRef} from 'react';
+ function Component() {}
+ export default forwardRef(Component);
+ `),
+ ).toMatchSnapshot();
+ });
+
it('registers likely HOCs with inline functions', () => {
expect(
transform(`
diff --git a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
index d851d72eb4c29..b8aca4e44226c 100644
--- a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+++ b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
@@ -274,6 +274,123 @@ describe('ReactFreshIntegration', () => {
}
});
+ it('switches memo to forwardRef with different inner', async () => {
+ if (__DEV__) {
+ await render(`
+ const Child = () => {
+ return
A1
;
+ };
+
+ export default Child;
+ `);
+ let lastElement = container.firstChild;
+ expect(lastElement.textContent).toBe('A1');
+ await patch(`
+ const {memo} = React;
+
+ const Child2 = () => {
+ return A2
;
+ };
+
+ export default memo(Child2);
+ `);
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A2');
+ lastElement = container.firstChild;
+
+ await patch(`
+ const {forwardRef} = React;
+
+ const Child3 = () => {
+ return A3
;
+ };
+
+ export default forwardRef(Child3);
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A3');
+ lastElement = container.firstChild;
+
+ await patch(`
+ const {memo} = React;
+
+ const Child4 = () => {
+ return A4
;
+ };
+
+ export default memo(Child4);
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A4');
+ lastElement = container.firstChild;
+
+ await patch(`
+ const Child5 = () => {
+ return A5
;
+ };
+
+ export default Child5;
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A5');
+ }
+ });
+
+ it('switches memo to forwardRef with same inner', async () => {
+ if (__DEV__) {
+ await render(`
+ const Child = () => {
+ return A1
;
+ };
+
+ export default Child;
+ `);
+ let lastElement = container.firstChild;
+ expect(lastElement.textContent).toBe('A1');
+ await patch(`
+ const {memo} = React;
+
+ const Child = () => {
+ return A1
;
+ };
+
+ export default memo(Child);
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A1');
+ lastElement = container.firstChild;
+
+ await patch(`
+ const {forwardRef} = React;
+
+ const Child = () => {
+ return A1
;
+ };
+
+ export default forwardRef(Child);
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A1');
+ lastElement = container.firstChild;
+
+ await patch(`
+ const Child = () => {
+ return A1
;
+ };
+
+ export default Child;
+ `);
+
+ expect(container.firstChild !== lastElement).toBe(true);
+ expect(container.firstChild.textContent).toBe('A1');
+ }
+ });
+
it('reloads default export with named memo', async () => {
if (__DEV__) {
await render(`
diff --git a/packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap b/packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
index 4f0979100339c..cc078bd75ad70 100644
--- a/packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
+++ b/packages/react-refresh/src/__tests__/__snapshots__/ReactFreshBabelPlugin-test.js.snap
@@ -268,11 +268,30 @@ const B = hoc(Foo);
_c4 = B;
var _c, _c2, _c3, _c4;
$RefreshReg$(_c, "Foo");
-$RefreshReg$(_c2, "%default%");
+$RefreshReg$(_c2, "%default%$hoc");
$RefreshReg$(_c3, "A");
$RefreshReg$(_c4, "B");
`;
+exports[`ReactFreshBabelPlugin registers forwardRef default export 1`] = `
+function Component() {}
+_c = Component;
+export default _c2 = React.forwardRef(Component);
+var _c, _c2;
+$RefreshReg$(_c, "Component");
+$RefreshReg$(_c2, "%default%$React.forwardRef");
+`;
+
+exports[`ReactFreshBabelPlugin registers forwardRef default export 2`] = `
+import { forwardRef } from 'react';
+function Component() {}
+_c = Component;
+export default _c2 = forwardRef(Component);
+var _c, _c2;
+$RefreshReg$(_c, "Component");
+$RefreshReg$(_c2, "%default%$forwardRef");
+`;
+
exports[`ReactFreshBabelPlugin registers identifiers used in JSX at definition site 1`] = `
import A from './A';
import Store from './Store';
@@ -397,6 +416,25 @@ $RefreshReg$(_c2, "%default%$React.memo");
$RefreshReg$(_c3, "%default%");
`;
+exports[`ReactFreshBabelPlugin registers memo default export 1`] = `
+function Component() {}
+_c = Component;
+export default _c2 = React.memo(Component);
+var _c, _c2;
+$RefreshReg$(_c, "Component");
+$RefreshReg$(_c2, "%default%$React.memo");
+`;
+
+exports[`ReactFreshBabelPlugin registers memo default export 2`] = `
+import { memo } from 'react';
+function Component() {}
+_c = Component;
+export default _c2 = memo(Component);
+var _c, _c2;
+$RefreshReg$(_c, "Component");
+$RefreshReg$(_c2, "%default%$memo");
+`;
+
exports[`ReactFreshBabelPlugin registers top-level exported function declarations 1`] = `
export function Hello() {
function handleClick() {}