react/only-export-components Restriction ​
What it does ​
Ensures that modules only export React components (and related HMR-safe items) so that Fast Refresh (a.k.a. hot reloading) can safely preserve component state. Concretely, it validates the shape of your module’s exports and common entrypoints (e.g. createRoot(...).render(<App />)
) to match what integrations like react-refresh
expect. The rule name is react-refresh/only-export-components
.
Why is this bad? ​
Fast Refresh can only reliably retain state if a module exports components and avoids patterns that confuse the refresh runtime. Problematic patterns (like export *
, anonymous default functions, exporting arrays of JSX, or mixing non-component exports in unsupported ways) can cause:
- Components to remount and lose state on edit
- Missed updates (no refresh) or overly broad reloads
- Fragile HMR behavior that differs between bundlers
By enforcing predictable exports, edits stay fast and stateful during development.
Examples ​
Examples of incorrect code for this rule:
// 1) Mixing util exports with components in unsupported ways
export const foo = () => {}; // util, not a component
export const Bar = () => <></>; // component
// 2) Anonymous default export (name is required)
export default function() {}
// 3) Re-exporting everything hides what’s exported
export * from "./foo";
// 4) Exporting JSX collections makes components non-discoverable
const Tab = () => null;
export const tabs = [<Tab />, <Tab />];
// 5) Bootstrapping a root within the same module that defines components
const App = () => null;
createRoot(document.getElementById("root")).render(<App />);
Examples of correct code for this rule:
// Named or default component exports are fine
export default function Foo() {
return null;
}
// Utilities may coexist if allowed by options or naming conventions
const foo = () => {};
export const Bar = () => null;
// Entrypoint files may render an imported component
import { App } from "./App";
createRoot(document.getElementById("root")).render(<App />);
Options (or not) ​
allowExportNames ​
{ type: string[], default: [] }
Treat specific named exports as HMR-safe (useful for frameworks that hot-replace certain exports). For example, in Remix:
{
"react/only-export-components": [
"error",
{ "allowExportNames": ["meta", "links", "headers", "loader", "action"] }
]
}
allowConstantExport ​
{ type: boolean, default: false }
Allow exporting primitive constants (string/number/boolean/template literal) alongside component exports without triggering a violation. Recommended when your bundler’s Fast Refresh integration supports this (enabled by the plugin’s vite
preset).
{
"react/only-export-components": [
"error",
{ "allowConstantExport": true }
]
}
// Allowed when allowConstantExport: true
export const VERSION = "3";
export const Foo = () => null;
customHOCs ​
{ type: string[], default: [] }
If you export components wrapped in custom higher-order components, list their identifiers here to avoid false positives:
{
"react/only-export-components": [
"error",
{ "customHOCs": ["observer", "withAuth"] }
]
}
checkJS ​
{ type: boolean, default: false }
Check .js
files that contain JSX (in addition to .tsx
/.jsx
). To reduce false positives, only files that import React are checked when this is enabled.
{
"react/only-export-components": ["error", { "checkJS": true }]
}
How to use ​
To enable this rule in the CLI or using the config file, you can use:
oxlint --deny react/only-export-components --react-plugin
{
"plugins": ["react"],
"rules": {
"react/only-export-components": "error"
}
}