eslint-plugin-react-refresh is an ESLint plugin that validates your components are structured so React's Fast Refresh can update them safely during development, and it does this through a single rule, react-refresh/only-export-components. If you have used create-vite with the React template, you have already been running it, because Vite's React scaffold enables it by default.
It is a small plugin with a narrow job, but the job matters more than its size suggests. This guide explains what it enforces, how it connects to the Fast Refresh machinery, and why a warning from it is worth reading rather than suppressing.
What Fast Refresh needs from your files
Fast Refresh, also called hot reloading, is the feature where editing a component updates only the changed part of the running page while preserving component state, instead of triggering a full reload. The bundler integration that makes this work, the react-refresh runtime, relies on a rule of thumb: a module should export React components and nothing else that would confuse the hot-update boundary.
When a file exports both a component and a non-component value, or exports components inconsistently, the runtime cannot reliably tell what to swap. The result is that Fast Refresh silently falls back to a full page reload, losing your component state. You lose the productivity feature without any error telling you why. That silent degradation is exactly what the plugin exists to surface.
The only-export-components rule
The plugin ships a single rule, react-refresh/only-export-components. It warns when a file exports things that break the Fast Refresh boundary. The classic trigger is mixing a component export with a constant export:
// Triggers the rule: a component and a non-component in one file
export const MyComponent = () => <div>hi</div>;
export const SOME_CONSTANT = 42;
The rule has options for the common exceptions. allowConstantExport permits exporting constants alongside components, which the Vite React plugin supports, so the pattern above becomes acceptable when that option is on. There are also allowances for specific export names used by frameworks. Next.js pages, for instance, legitimately export functions like getServerSideProps next to a default component, and the plugin's framework presets account for that.
A typical flat-config setup looks like this:
import reactRefresh from "eslint-plugin-react-refresh";
export default [
{
plugins: { "react-refresh": reactRefresh },
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
];
Recent releases target ESLint 9 and the flat config format, and the package is distributed as ESM. If you are still on the legacy .eslintrc format or an older ESLint, pin a compatible plugin version and read the changelog before upgrading, because the config surface changed with the flat-config transition.
How it relates to the webpack plugin
There is a common point of confusion worth clearing up. eslint-plugin-react-refresh is a linter plugin. It does not perform hot reloading itself. The actual Fast Refresh transform is done by a bundler plugin: @pmmmwh/react-refresh-webpack-plugin for webpack (often referenced simply as the react refresh webpack plugin or react-refresh-webpack-plugin), and the built-in React Fast Refresh support in Vite's React plugin.
So the division of labor is:
- The
react-refresh-webpack-plugin(the@pmmmwh/react-refresh-webpack-pluginpackage) or Vite's React plugin does the runtime work of swapping components in place. eslint-plugin-react-refreshstatically checks your source so the runtime work does not silently fail.
You need the bundler plugin for Fast Refresh to function at all. You add the ESLint plugin so you get told, at lint time, when a file is written in a way that would break it. They complement each other; neither replaces the other.
Why a linter plugin appears in a security guide
On its face this is a developer-experience tool, not a security control. But there are two threads worth pulling for anyone thinking about supply chain hygiene.
First, every dev dependency is attack surface. A linter plugin runs in your development environment and your CI with access to your source. The npm ecosystem has repeatedly seen postinstall-script and account-takeover incidents in build-time packages, and a plugin like this, while small and reputable, is still a link in your dependency chain. Pin it, review its transitive dependencies, and treat updates to your lint tooling with the same lockfile discipline you apply to runtime packages. A tool such as Safeguard can flag when a dev dependency picks up a risky transitive package, which is where these issues usually hide.
Second, the discipline the rule enforces (clean module boundaries, one concern per file) makes code easier to reason about, and code that is easier to reason about is easier to review for actual security bugs. A file that mixes a component, a constant, a data-fetching helper, and a side-effecting export is harder to audit than one that exports a single, well-scoped thing. The Fast Refresh rule nudges you toward the reviewable version almost by accident.
Should you treat it as warn or error?
Most teams run it as a warning, and that is the right default. A Fast Refresh boundary problem does not break your production build or your tests; it only degrades the dev experience. Making it an error would fail CI on something that has no runtime consequence, which trains developers to disable rules. Keep it as warn, let developers see it while they work, and let them fix files opportunistically. Reserve error for rules that catch actual correctness or security defects.
If you find the rule firing constantly in a mature codebase, that is usually a signal your files are doing too much, and it is worth a small refactor to split component modules from utility modules. For a broader treatment of keeping your JavaScript toolchain trustworthy, our Academy covers dependency review practices for front-end teams.
FAQ
What does eslint-plugin-react-refresh actually check?
It provides one rule, react-refresh/only-export-components, which warns when a module exports non-component values alongside components in a way that breaks React Fast Refresh's hot-update boundary. When that boundary breaks, Fast Refresh silently falls back to a full reload and loses component state.
Is eslint-plugin-react-refresh the same as the react-refresh webpack plugin?
No. eslint-plugin-react-refresh is a linter that statically checks your source. The actual hot-reloading runtime is provided by a bundler plugin, @pmmmwh/react-refresh-webpack-plugin for webpack or the built-in support in Vite's React plugin. You need the bundler plugin for Fast Refresh to work; the ESLint plugin just warns when your code would break it.
Should the rule be a warning or an error?
Warning. A Fast Refresh boundary issue only affects the development experience, not your production build or tests. Configuring it as an error fails CI on something with no runtime consequence, which encourages developers to disable it. Keep it as a warning and fix files opportunistically.
Does it work with ESLint 9 and flat config?
Recent versions target ESLint 9 and the flat config format and ship as ESM. If you are on the legacy configuration format or an older ESLint, pin a compatible plugin version and check the changelog before upgrading, since the config surface changed with the flat-config transition.