Программирование

Почему ESLint ругается Fast refresh only exports components

Разбор правила react-refresh/only-export-components из eslint-plugin-react-refresh: почему срабатывает на неэкспортируемых компонентах в .tsx файлах, примеры FAIL/PASS, false positives, опции allowConstantExport и workarounds для Vite/Next.js.

4 ответа 1 просмотр

Почему ESLint выдает предупреждение «Fast refresh only works when a file only exports components» в файле, который экспортирует только функции и переменные, но содержит неэкспортируемый компонент? Это ложное срабатывание или нужно выносить внутренние компоненты в отдельный файл?

Пример кода:

jsx
function Component() {
 return <div />;
}

export const stuff = {
 render: () => <Component />;
};

Правило react-refresh/only-export-components из eslint-plugin-react-refresh срабатывает на файлах с расширением .tsx. Нельзя ли избежать этого, если компоненты объявлены внутри файла?

Кроме того, в документации приведен пример, помеченный как FAIL, хотя там нет экспортов:

jsx
const App = () => {};
createRoot(document.getElementById("root")).render(<App />);

Почему этот пример считается нарушением правила?

Правило react-refresh/only-export-components из eslint-plugin-react-refresh срабатывает на файлах .tsx, потому что обнаруживает React-компоненты (даже внутренние, неэкспортируемые), но видит экспорты non-components вроде объектов или функций — это не ложное срабатывание, а защита от проблем с react refresh (Fast Refresh). В вашем примере Component внутри файла триггерит правило, несмотря на отсутствие его экспорта, так как eslint react refresh требует, чтобы весь файл был “чистым” для компонентов. Пример из документации с createRoot и локальным App тоже FAIL, потому что файл содержит компонент, но не экспортирует его как основной — Fast Refresh ожидает строго компонентные модули.


Содержание


Что такое react refresh и правило eslint react refresh/only-export-components

React refresh, или Fast Refresh, — это суперудобная фича для разработки React-приложений в Vite, Webpack или Next.js. Она обновляет компоненты на лету, без полной перезагрузки страницы, сохраняя состояние. Но работает идеально только в “чистых” файлах, где экспортируется ровно один компонент или несколько, но без лишнего: никаких утилит, констант или объектов.

Здесь на сцену выходит правило react-refresh/only-export-components из пакета eslint-plugin-react-refresh. Автор плагина, Arnaud Barre (мейнтейнер Vite React-плагинов), создал его специально для bundler’ов вроде Vite. Оно сканирует .tsx/.jsx файлы и ругается, если находит React-компоненты (PascalCase + JSX/TSX), но экспорты — не компоненты. Почему? Fast Refresh HMR (Hot Module Replacement) может сломаться на mixed exports: компонент обновится криво, если рядом utils.

В вашем случае файл экспортирует stuff (объект с рендером), а внутри Component. Правило видит компонент — и бац, предупреждение “Fast refresh only works when a file only exports components”. Это не баг ESLint, а сознательный дизайн: лучше предупредить заранее, чем дебажить HMR в продакшене.

А представьте: вы пишете entry-point вроде main.tsx с createRoot, и там локальный компонент. Fast Refresh подумает, что это модуль для hot-reload, но без экспорта — хаос. Именно поэтому документация метит такие примеры FAIL.


Почему срабатывает предупреждение на неэкспортируемых компонентах

Коротко: правило не смотрит, экспортируете ли вы компонент. Оно проверяет наличие любых компонентов в файле. Если они есть (по сигнатуре: uppercase имя + JSX), то все экспорты должны быть компонентами. Ваш код:

jsx
function Component() { // ← Это компонент! PascalCase + JSX внутри
 return <div />;
}

export const stuff = { // ← Non-component экспорт
 render: () => <Component />;
};

Триггер на Component, потому что stuff — объект. Даже если Component приватный, ESLint парсит весь AST. Это не false positive: документация объясняет, что Fast Refresh оптимизирован под “component-only modules”. Смешанные файлы приводят к partial updates, где состояние теряется или UI мерцает.

Теперь про пример из доки:

jsx
const App = () => {}; // Компонент!
createRoot(document.getElementById("root")).render(<App />);

Нет экспортов совсем! Но FAIL. Почему? Файл содержит компонент, но не экспортирует его. Правило предполагает: если компонент есть, файл должен быть для HMR — экспортируйте его, или выносите в отдельный модуль. Без экспорта Fast Refresh игнорирует файл, но ESLint предупреждает о потенциальной проблеме. В реале такие entry-points (main.tsx) часто вызывают issues в Vite+React.

Выносить внутренние компоненты обязательно? Не всегда. Но для стабильного react fast refresh — да, рекомендуется. Или используйте опции/дизейбл.


Примеры FAIL и PASS из документации react refresh plugin

Документация eslint-plugin-react-refresh полна кодов. Разберём ключевые.

FAIL (как ваш):

jsx
const MyComponent = () => <div />; // Компонент

export const utils = { fn: () => {} }; // Non-component

Или без экспортов:

jsx
const App = () => <div />;
createRoot(...).render(<App />); // FAIL!

PASS варианты:

  1. Только компонент:
jsx
export const MyComponent = () => <div />;
  1. Несколько компонентов:
jsx
export const A = () => <div />;
export const B = () => <div />;
  1. Импорт извне:
jsx
import { MyComponent } from './other';
createRoot(...).render(<MyComponent />);

Секрет в сигнатуре: правило детектит функции/классы с JSX и PascalCase. Class components? Поддержка слабая, если нет direct export. export * тоже не всегда работает.

В вашем stuff.render — JSX рендерит Component, но stuff сам не компонент. Отсюда warning.


False positives и быстрые workarounds для eslint plugin react refresh

Да, бывают false positives. Классика из issue #25: React.lazy + роутеры.

tsx
const LazyComp = React.lazy(() => import('./Comp.tsx'));
export const router = createBrowserRouter([...]); // Non-component!

Тут warning, хотя lazy — динамика, и react refresh webpack plugin (или Vite) справляется. Ещё: ALL-UPPERCASE константы, конфиги.

Workarounds (без вреда HMR):

  1. Дизейбл на файле:
jsx
/* eslint-disable react-refresh/only-export-components */
export const stuff = { ... };

Автор плагина рекомендует для lazy/router. Не ломает react refresh.

  1. Inline дизейбл:
jsx
// eslint-disable-next-line react-refresh/only-export-components
export const stuff = { ... };
  1. Wrapper-компонент:
jsx
export const StuffWrapper = () => {
 const stuff = { render: () => <Component /> };
 return null; // Или используйте
};

Но это костыль.

SO-пост от xNevrroXx подтверждает: disable безопасен для mixed файлов.


Настройка опций: allowConstantExport и allowExportNames

Не хотите дизейблить? Конфиг в .eslintrc:

json
{
 "plugins": ["react-refresh"],
 "rules": {
 "react-refresh/only-export-components": ["error", {
 "allowConstantExport": true, // Разрешает const exports (Vite-friendly)
 "allowExportNames": ["router", "stuff"] // Белый список имён
 }]
 }
}
  • allowConstantExport: true — для констант вроде вашего stuff. Идеально для Vite.
  • allowExportNames — для фреймворков (Next.js routers).

В доке: опции эволюционировали, проверяйте версию. Для eslint react refresh vite — добавьте в vite.config.ts:

ts
import reactRefresh from '@vitejs/plugin-react';
export default { plugins: [reactRefresh()] };

Webpack? Аналогично с react-refresh-webpack-plugin.


Лучшие практики рефакторинга для Vite, Next.js и Webpack

Идеал: разделяйте файлы.

  • Компоненты — отдельно: Component.tsxexport const Component = ...
  • Utils/entry — без JSX: main.tsx только createRoot(<App />) с импортом.

Рефакторинг вашего примера:

tsx
// Component.tsx
export function Component() { return <div />; }

// stuff.ts (не .tsx!)
export const stuff = {
 render: () => <Component /> // ESLint не парсит JSX здесь
};

Для роутеров (React Router/Next): выносите в routes.tsx с wrapper’ом или опциями.

Vite/Next.js: правило must-have для DX. Webpack с react refresh webpack plugin — то же.

А если лень? Дизейбл + опции. Но чистые модули = стабильный Fast Refresh. В проде это спасает часы дебага.


Источники

  1. eslint-plugin-react-refresh README — Полная документация правила react-refresh/only-export-components с примерами FAIL/PASS: https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/README.md
  2. Issue #25: React.lazy false positive — Обсуждение ложных срабатываний с lazy-импортами и роутерами в Vite: https://github.com/ArnaudBarre/eslint-plugin-react-refresh/issues/25
  3. Stack Overflow: Avoid ESLint Fast Refresh warning — Практические решения для файлов с non-component экспортами: https://stackoverflow.com/questions/77365777/how-to-avoid-eslint-warning-in-react-fast-refresh-only-works-when-a-file-only-e

Заключение

Правило react-refresh/only-export-components — не прихоть, а гарант стабильного react refresh в eslint react refresh экосистеме: оно ловит mixed файлы до того, как HMR сломается. Ваш случай и док-пример FAIL из-за локальных компонентов без чистого экспорта — выносите в отдельные файлы или настраивайте allowConstantExport/дизейбл. Лучшая практика: компоненты отдельно, utils без JSX. Это ускорит разработку в Vite/Next.js, сэкономит нервы. Попробуйте опции — и warning уйдёт без костылей.

Arnaud Barre / Разработчик плагинов для Vite и ESLint

Правило react-refresh/only-export-components из eslint-plugin-react-refresh обеспечивает правильную структуру файлов для React Fast Refresh в сборщиках вроде Vite или Webpack. Оно применяется только к файлам .tsx/.jsx, чтобы избежать ложных срабатываний. Предупреждение возникает, если файл содержит компоненты (PascalCase + JSX), но экспортирует некомпоненты (константы, утилиты, объекты) — даже неэкспортируемые компоненты блокируют, как в FAIL-примере с const App и createRoot.render(<App />), поскольку файл не экспортирует ТОЛЬКО компоненты.

PASS-случаи: экспорт только компонентов или импорт извне. Опции конфигурации: allowConstantExport для констант (Vite), allowExportNames для фреймворков вроде React Router.

X

Это false positive правила eslint react refresh в файлах с React.lazy и экспортом router (некомпоненты). Автор плагина подтверждает проблему в issue #25. Workarounds:

  • /* eslint-disable react-refresh/only-export-components */ для файла (не ломает react fast refresh);
  • Экспортировать wrapper-компонент вместо router.

Не выносите внутренние компоненты — disable безопасен. Пример: lazy(() => import("./Component.tsx")) + export {router} триггерит на .tsx.

@bsnman / Full-stack разработчик

В issue #25 обсуждается false positive react refresh plugin при React.lazy в Vite + React + TS: предупреждение на файлах с lazy-импортами компонентов и non-component экспортами (routers). Автор eslint plugin react refresh рекомендует ESLint-disable для таких файлов или рефакторинг на экспорт компонента-обертки. Не влияет на react fast refresh. Ограничения правила: нет поддержки export *, class components, ALL-UPPERCASE без прямого экспорта.

Авторы
Arnaud Barre / Разработчик плагинов для Vite и ESLint
Разработчик плагинов для Vite и ESLint
X
Разработчик
J
Программный разработчик
@bsnman / Full-stack разработчик
Full-stack разработчик
Проверено модерацией
НейроОтветы
Модерация