feat(transformer/react-refresh): support module.property.useHook() (#23190)

## Summary

Ports facebook/react#35318 to the React Refresh plugin: hook calls through a nested member expression such as `FancyHook.property.useNestedThing()` are now collected into the `$RefreshSig$` custom-hooks list, so components using them no longer lose state on every fast-refresh update. As in the Babel plugin, the binding is resolved through one extra static member level, and a force reset is emitted when the root object has no binding in scope. Deeper nesting (`A.B.C.useHook()`) remains uncollected, same as upstream; note that Babel additionally emits `forceReset = true` in that case while oxc does not — a pre-existing, intentional divergence from #6143 that this PR does not change.

closes #16571

## Example

Input:

```jsx
import FancyHook from 'fancy';

export default function App() {
  const foo = FancyHook.property.useNestedThing();
  return <h1>{foo}</h1>;
}
```

Signature before:

```js
_s(App, "useNestedThing{foo}");
```

Signature after:

```js
_s(App, "useNestedThing{foo}", false, function() {
  return [FancyHook.property.useNestedThing];
});
```

---

This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
This commit is contained in:
Dunqing committed 2026-06-12 02:04:52 +00:00
1 parent 150bce13e4
commit 6c0bdf0486
6 files changed
+66 -10

No files matched your search

+29 -8
View File
@@ -331,15 +331,27 @@ impl<'a> Traverse<'a, TransformState<'a>> for ReactRefresh<'a> {
if !is_builtin_hook(&hook_name) {
// Check if a corresponding binding exists where we emit the signature.
let (binding_name, is_member_expression): (Option<Ident>, _) = match &call_expr.callee {
Expression::Identifier(ident) => (Some(ident.name), false),
Expression::StaticMemberExpression(member) => {
if let Expression::Identifier(object) = &member.object {
(Some(object.name), true)
} else {
(None, false)
let (binding_name, middle_property, is_member_expression): (
Option<Ident>,
Option<Ident>,
bool,
) = match &call_expr.callee {
Expression::Identifier(ident) => (Some(ident.name), None, false),
Expression::StaticMemberExpression(member) => match &member.object {
// `FancyHook.useHook()`
Expression::Identifier(object) => (Some(object.name), None, true),
// `FancyHook.property.useHook()`.
// Like the upstream Babel plugin (facebook/react#35318), only one
// extra member level is supported; deeper nesting is not collected.
Expression::StaticMemberExpression(inner_member) => {
if let Expression::Identifier(object) = &inner_member.object {
(Some(object.name), Some(inner_member.property.name), true)
} else {
(None, None, false)
}
}
}
_ => (None, None, false),
},
_ => unreachable!(),
};
@@ -359,6 +371,15 @@ impl<'a> Traverse<'a, TransformState<'a>> for ReactRefresh<'a> {
);
if is_member_expression {
if let Some(middle_property) = middle_property {
// binding_name.middle_property
expr = Expression::from(ctx.ast.member_expression_static(
SPAN,
expr,
ctx.ast.identifier_name(SPAN, middle_property),
false,
));
}
// binding_name.hook_name
expr = Expression::from(ctx.ast.member_expression_static(
SPAN,
@@ -1,6 +1,6 @@
commit: 2688fbd1
Passed: 237/395
Passed: 239/397
# All Passed:
* babel-plugin-transform-class-static-block
@@ -664,7 +664,7 @@ after transform: [ReferenceId(0), ReferenceId(1), ReferenceId(4), ReferenceId(9)
rebuilt : [ReferenceId(5)]
# babel-plugin-transform-react-jsx (49/52)
# babel-plugin-transform-react-jsx (51/54)
* refresh/import-after-component/input.js
Missing ScopeId
Missing ReferenceId: "useFoo"
@@ -0,0 +1,4 @@
export function App() {
const foo = GlobalHook.property.useNestedThing();
return <h1>{foo}</h1>;
}
@@ -0,0 +1,11 @@
import { jsx as _jsx } from "react/jsx-runtime";
var _s = $RefreshSig$();
export function App() {
_s();
const foo = GlobalHook.property.useNestedThing();
return /* @__PURE__ */ _jsx("h1", { children: foo });
}
_s(App, "useNestedThing{foo}", true);
_c = App;
var _c;
$RefreshReg$(_c, "App");
@@ -0,0 +1,6 @@
import FancyHook from 'fancy';
export default function App() {
const foo = FancyHook.property.useNestedThing();
return <h1>{foo}</h1>;
}
@@ -0,0 +1,14 @@
import FancyHook from "fancy";
import { jsx as _jsx } from "react/jsx-runtime";
var _s = $RefreshSig$();
export default function App() {
_s();
const foo = FancyHook.property.useNestedThing();
return /* @__PURE__ */ _jsx("h1", { children: foo });
}
_s(App, "useNestedThing{foo}", false, function() {
return [FancyHook.property.useNestedThing];
});
_c = App;
var _c;
$RefreshReg$(_c, "App");