The react-refresh transform does not detect React components when the arrow function initializer is wrapped in parentheses.
I noticed this after upgrading vite to 8.0.1 and @vitejs/plugin-react to version 6.0.1, which now uses Oxc instead of Babel. This causes Vite to fall back to a full page reload instead of hot module replacement.
Reproduction
// HMR works
const App = () => (<div>Hello</div>);
// HMR does NOT work – full page reload
const App = (() => (<div>Hello</div>));
export { App };
Both are semantically identical. The only different is the outer parentheses wrapping the arrow function expression.
I know...
I acknowledge that, at first glance, this may seem like a silly thing to fix, and the outer parentheses are more-so my own stylistic choice.
However, it is important to note that parenthesized expressions are semantically transparent. (expr) and expr are identical in JavaScript/TypeScript. I believe it fair to say that any AST-based tool that changes behavior based on their presence has a bug – it violates the language spec.
Suggested fix
Note I am not familiar with the oxc source, nor am I familiar with Rust. I hope my explanation makes sense.
Unwrap any parenthesized expressions before checking the arrow/function expression.
Workaround
Hopefully the following helps anyone who stumbles across this issue!
Here's a simple Vite plugin that strips outer wrapping parentheses:
// OXC's react-refresh doesn't unwrap parenthesized arrow functions.
// So, `const foo = ((): T => ...)` never gets registered for HMR.
const oxcRefreshWorkaround = ((): Plugin => {
const findMatchingClose = ((code: string, open: number): number => {
let depth = 1;
let i = (open + 1);
while(i < code.length) {
if(code[i] === '(') {
depth++;
} else if((code[i] === ')') && (--depth === 0)) {
return i;
}
i++;
}
return -1;
});
return ({
name: 'oxc-refresh-workaround',
enforce: 'pre',
transform: ((code, id) => {
if(!/\.[tj]sx$/.test(id)) {
return;
}
const re = /\b(?:const|let)\s+[A-Z]\w*\s*=\s*\(/g;
let out = '';
let cursor = 0;
let m;
while((m = re.exec(code)) !== null) {
const open = ((m.index + m[0].length) - 1);
let next = (open + 1);
while((code[next] === ' ') || (code[next] === '\n') || (code[next] === '\r') || (code[next] === '\t')) {
next++;
}
if(code[next] !== '(') {
continue;
}
const close = findMatchingClose(code, open);
if(close === -1) {
continue;
}
out += code.slice(cursor, open);
out += code.slice((open + 1), close);
cursor = (close + 1);
re.lastIndex = cursor;
}
if(cursor === 0) {
return;
}
out += code.slice(cursor);
return out;
})
});
});
The react-refresh transform does not detect React components when the arrow function initializer is wrapped in parentheses.
I noticed this after upgrading
viteto8.0.1and@vitejs/plugin-reactto version6.0.1, which now uses Oxc instead of Babel. This causes Vite to fall back to a full page reload instead of hot module replacement.Reproduction
Both are semantically identical. The only different is the outer parentheses wrapping the arrow function expression.
I know...
I acknowledge that, at first glance, this may seem like a silly thing to fix, and the outer parentheses are more-so my own stylistic choice.
However, it is important to note that parenthesized expressions are semantically transparent.
(expr)andexprare identical in JavaScript/TypeScript. I believe it fair to say that any AST-based tool that changes behavior based on their presence has a bug – it violates the language spec.Suggested fix
Note I am not familiar with the oxc source, nor am I familiar with Rust. I hope my explanation makes sense.
Unwrap any parenthesized expressions before checking the arrow/function expression.
Workaround
Hopefully the following helps anyone who stumbles across this issue!
Here's a simple Vite plugin that strips outer wrapping parentheses: