Describe the bug
When transform.react.refresh is enabled and a function recognised as a React custom hook
(useFoo) recursively calls itself in its own body, SWC's react-refresh transform emits a
registration call whose getCustomHooks callback returns an array that includes useFoo itself:
_s(useFoo, "<sigKey>", false, function () {
return [useFoo];
});
useFoo should not appear in its own dependency list — those entries are meant to be the
other custom hooks that useFoo depends on, used by Fast Refresh's family-graph signature
algorithm. Babel's react-refresh/babel plugin filters self-references; SWC does not.
The runtime consequence is fatal. react-refresh-runtime's computeFullKey(family) walks family.getCustomHooks() and recursively computes computeFullKey(child.family)
for each entry. With a self-reference the walk hits the same family twice, but family.fullKey
is only memoised at the end of the outer call — so the inner call sees fullKey === null and
recurses again. Every component that imports the offending hook stack-overflows during module
load with Maximum call stack size exceeded.
Input code
import { useState, useEffect } from 'react';
export function useFoo(deps) {
const [x, setX] = useState();
useEffect(() => { setX(1); }, []);
const y = deps ? useFoo(deps) : null;
return y || x;
}
Config
{
"jsc": {
"parser": { "syntax": "ecmascript", "jsx": true },
"transform": {
"react": {
"runtime": "automatic",
"development": true,
"refresh": true
}
}
},
"isModule": true
}
Link to the code that reproduces this issue
https://play.swc.rs/?version=1.15.32&code=H4sIAAAAAAAAA1XNsQqDMBDG8T1P8W1NwKVrg%2B3UvkCXgjhIegFBE0kuYFHfvYkFodvB%2Fe5%2F%2FTj5wFiQIj25Y6rKdLeWDGODDX7EKVBn%2BKSFoHnXNjnDvXeFPryXb5qiwiIA411kNHOFSPxqUR9dqXTeH20pFepr%2FlucPCuNrULT7ugX%2BeTjEsbt%2F80FLg1DcYE4BZfhumLWYhNfqP3D080AAAA%3D&config=H4sIAAAAAAAAA02OQQ7CMAwE731F5HMOqEf%2BwCOs1IWgJqlsB4Gq%2Fr1JCaUne2e1ay%2BdMfAUB1ezlLWIGVmIqzYgn6j4LjuQCyiO%2Faxga6BC5Uxmtd%2BYMkYZE4ejqUAmdHoCFeWoPlDtxKwpoHoH9u8P9KIpzYGithMnk2lkkkczGl%2F3efyBfCfdX5b%2B0vfQNQ%2B83NKQJ%2Fql1w2wMxLK%2FAAAAA%3D%3D
SWC Info output
Expected behavior
The emitted getCustomHooks array must not contain useFoo (the function being registered).
Babel's react-refresh/babel plugin produces:
var _s = $RefreshSig$();
function useFoo(deps) {
_s();
var _useState = _slicedToArray(useState(), 2),
x = _useState[0],
setX = _useState[1];
useEffect(function () { setX(1); }, []);
var y = deps ? useFoo(deps) : null;
return y || x;
}
_s(useFoo, "evHy7IFA8+DTDHwDqNbBVPj7xeE=", false, function () {
return [];
});
Note the empty array — Babel correctly excludes the self-reference. (useState and useEffect
are built-in React hooks and are also correctly excluded.)
Actual behavior
SWC (1.15.32) emits the self-reference:
var _s = $RefreshSig$();
export function useFoo(deps) {
_s();
var _useState = _sliced_to_array(useState(), 2), x = _useState[0], setX = _useState[1];
useEffect(function() { setX(1); }, []);
var y = deps ? useFoo(deps) : null;
return y || x;
}
_s(useFoo, "evHy7IFA8+DTDHwDqNbBVPj7xeE=", false, function() {
return [
useFoo // <-- self-reference; should not be here
];
});
When this output is loaded under react-refresh-runtime.development.js the very first call to
_s() (from inside useFoo's body) triggers computeFullKey(useFoo.family), which walks
getCustomHooks() and recurses into itself. Result: RangeError: Maximum call stack size exceeded
in react-refresh-runtime's y function at module load.
Version
@swc/core 1.15.32
Additional context
- The Babel reference plugin that filters self-references is
react-refresh/babel.
The relevant filter happens in getHookCallsForCustomHook (or similar — name varies by version);
it walks the function body, collects call sites that look like custom hooks, and explicitly skips
any reference whose binding resolves to the function being analysed.
- We hit this in
@legendapp/state v3 (useObservable(initialValue, deps) recurses on deps),
but it reproduces with any user-defined useX whose body conditionally calls itself, but seems to only happen when minified.
- Filtering should match Babel's: identifiers in the callback's returned array literal whose
binding resolves to the function passed as _s's first argument get dropped. Member-expression
or call-expression entries (_state.useFoo, useFoo()) are not affected because Babel doesn't
emit them and SWC's bug is also limited to the bare-identifier case.
Describe the bug
When
transform.react.refreshis enabled and a function recognised as a React custom hook(
useFoo) recursively calls itself in its own body, SWC's react-refresh transform emits aregistration call whose
getCustomHookscallback returns an array that includesuseFooitself:useFooshould not appear in its own dependency list — those entries are meant to be theother custom hooks that
useFoodepends on, used by Fast Refresh's family-graph signaturealgorithm. Babel's
react-refresh/babelplugin filters self-references; SWC does not.The runtime consequence is fatal.
react-refresh-runtime'scomputeFullKey(family)walksfamily.getCustomHooks()and recursively computescomputeFullKey(child.family)for each entry. With a self-reference the walk hits the same family twice, but
family.fullKeyis only memoised at the end of the outer call — so the inner call sees
fullKey === nullandrecurses again. Every component that imports the offending hook stack-overflows during module
load with
Maximum call stack size exceeded.Input code
Config
{ "jsc": { "parser": { "syntax": "ecmascript", "jsx": true }, "transform": { "react": { "runtime": "automatic", "development": true, "refresh": true } } }, "isModule": true }Link to the code that reproduces this issue
https://play.swc.rs/?version=1.15.32&code=H4sIAAAAAAAAA1XNsQqDMBDG8T1P8W1NwKVrg%2B3UvkCXgjhIegFBE0kuYFHfvYkFodvB%2Fe5%2F%2FTj5wFiQIj25Y6rKdLeWDGODDX7EKVBn%2BKSFoHnXNjnDvXeFPryXb5qiwiIA411kNHOFSPxqUR9dqXTeH20pFepr%2FlucPCuNrULT7ugX%2BeTjEsbt%2F80FLg1DcYE4BZfhumLWYhNfqP3D080AAAA%3D&config=H4sIAAAAAAAAA02OQQ7CMAwE731F5HMOqEf%2BwCOs1IWgJqlsB4Gq%2Fr1JCaUne2e1ay%2BdMfAUB1ezlLWIGVmIqzYgn6j4LjuQCyiO%2Faxga6BC5Uxmtd%2BYMkYZE4ejqUAmdHoCFeWoPlDtxKwpoHoH9u8P9KIpzYGithMnk2lkkkczGl%2F3efyBfCfdX5b%2B0vfQNQ%2B83NKQJ%2Fql1w2wMxLK%2FAAAAA%3D%3D
SWC Info output
Expected behavior
The emitted
getCustomHooksarray must not containuseFoo(the function being registered).Babel's
react-refresh/babelplugin produces:Note the empty array — Babel correctly excludes the self-reference. (
useStateanduseEffectare built-in React hooks and are also correctly excluded.)
Actual behavior
SWC (1.15.32) emits the self-reference:
When this output is loaded under
react-refresh-runtime.development.jsthe very first call to_s()(from insideuseFoo's body) triggerscomputeFullKey(useFoo.family), which walksgetCustomHooks()and recurses into itself. Result:RangeError: Maximum call stack size exceededin
react-refresh-runtime'syfunction at module load.Version
@swc/core 1.15.32
Additional context
react-refresh/babel.The relevant filter happens in
getHookCallsForCustomHook(or similar — name varies by version);it walks the function body, collects call sites that look like custom hooks, and explicitly skips
any reference whose binding resolves to the function being analysed.
@legendapp/statev3 (useObservable(initialValue, deps)recurses ondeps),but it reproduces with any user-defined
useXwhose body conditionally calls itself, but seems to only happen when minified.binding resolves to the function passed as
_s's first argument get dropped. Member-expressionor call-expression entries (
_state.useFoo,useFoo()) are not affected because Babel doesn'temit them and SWC's bug is also limited to the bare-identifier case.