react/only-export-components

Require files with components to export only components, so Fast Refresh works

Fast Refresh swaps an edited component into the running page and keeps its state. It can only do that for a file where every export is a component. If the file also exports a function, an object, or a context, each edit reloads the whole page and the state is lost.

The rule only checks .jsx and .tsx files. It skips files with .test., .spec., .cy., or .stories. in their name. An export counts as a component if its name starts with a capital letter and its value can be one, such as a function, a class that extends Component, or a call to memo, forwardRef, or lazy.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"react/only-export-components" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[lint.rules]
"react/only-export-components" = ["error", {
  allowConstantExport = false,
  allowExportNames = [], # list of strings
  allowCompoundComponents = false,
  customHOCs = [], # list of strings
  checkJS = false,
}]

Examples#

example.jsx1 problem
export function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}

export function Price({ cents }) {
  return <span>{formatPrice(cents)}</span>;
}

The rule also reports a component without a name, export *, a context next to a component, and a component in a file that doesn't export any components:

example.jsx1 problem
export default () => <p>Loading</p>;
example.jsx1 problem
export * from "./buttons";
example.jsx1 problem
import { createContext } from "react";

export const ThemeContext = createContext("light");

export function ThemeLabel({ theme }) {
  return <span>{theme}</span>;
}
example.jsx1 problem
const Row = ({ item }) => <li>{item.name}</li>;

export const renderRows = items => items.map(item => <Row key={item.id} item={item} />);

Not reported:

format.js
export function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}
example.jsxNo problems
import { memo } from "react";
import { formatPrice } from "./format.js";

export function Price({ cents }) {
  return <span>{formatPrice(cents)}</span>;
}

export const Total = memo(function Total({ cents }) {
  return <strong>{formatPrice(cents)}</strong>;
});

Options#

OptionDefaultWhat it does
allowConstantExportfalseAllow exports of strings, numbers, booleans, templates, and expressions like 60 * 1000.
allowExportNames[]Names that a file can always export, such as loader or meta in a framework's route files.
allowCompoundComponentsfalseTreat an object where every property is a component as a component.
customHOCs[]Functions that return a component, in addition to memo, forwardRef, and lazy.
checkJSfalseAlso check .js files. With this option, only files that import something from "react" are checked.

Not reported with allowConstantExport: true:

example.jsxwith { "allowConstantExport": true }No problems
export const PAGE_SIZE = 20;

export function Page({ items }) {
  return <ul>{items.slice(0, PAGE_SIZE).map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

With allowExportNames: ["loader"]:

example.jsxwith { "allowExportNames": ["loader"] }No problems
export async function loader() {
  return fetch("/api/user");
}

export function Profile({ user }) {
  return <h1>{user.name}</h1>;
}

With allowCompoundComponents: true:

example.jsxwith { "allowCompoundComponents": true }No problems
const Root = ({ children }) => <div role="tablist">{children}</div>;
const Tab = ({ label }) => <button role="tab">{label}</button>;

export const Tabs = { Root, Tab };

With customHOCs: ["observer"]:

example.jsxwith { "customHOCs": ["observer"] }No problems
import { observer } from "mobx-react-lite";

export default observer(function Cart({ cart }) {
  return <span>{cart.count}</span>;
});