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#

Reported:

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:

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

export const ThemeContext = createContext("light");

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

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

Not reported:

export function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}
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:

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"]:

export async function loader() {
  return fetch("/api/user");
}

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

With allowCompoundComponents: true:

const Root = ({ children }) => <div role="tablist">{children}</div>;
const Tab = ({ label }) => <button role="tab">{label}</button>;

export const Tabs = { Root, Tab };

With customHOCs: ["observer"]:

import { observer } from "mobx-react-lite";

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