This article is published in English.
React 19 Lint Rules on ESLint 10 When eslint-plugin-react Lags Behind
Why eslint-plugin-react breaks on ESLint 10, how a Biome-first setup narrows React linting to 11 rules, and how to wire the fork into flat config and Next.js.
Upgrading a React codebase to ESLint 10 often stalls on a single dependency: eslint-plugin-react. At the time of writing, its latest release does not support ESLint 10, and the upstream fix is still waiting to be merged. This article explains the failure, walks through how an independent fork, @ternaus/eslint-plugin-react, cut the rule set down to the React 19 checks that still matter once Biome is doing most of the linting, and shows how to install it in plain flat config and in Next.js.
Why lint rules matter more when agents write code
The more work a team hands to coding agents, the more of its repository expectations need to be executable. Human review is an expensive place to discover a predictable mistake. Pre-commit hooks, tests, deterministic convention checks and small, reviewable commits turn expectations into pass/fail signals an agent can act on, and keep each failure small enough to diagnose. Linting is one of those guardrails, which is why losing the React lint layer during a toolchain upgrade is more than an inconvenience.
What breaks under ESLint 10
Among the breaking changes in the ESLint 10 release is the removal of long-deprecated methods on the rule context object. The newest published version of the React plugin, eslint-plugin-react@7.37.5, lists ESLint 9 as its highest supported peer, and some of its rules still call those methods. Under ESLint 10 that surfaces as a crash like this:
TypeError: contextOrFilename.getFilename is not a function
The method in question, context.getFilename(), was replaced by the context.filename property in the modern rule API, so the plugin code has to change; no configuration flag restores it.
Upstream tracks the problem in a GitHub issue filed on February 7, 2026; a candidate fix arrived as a pull request on July 30. Neither had been closed as of late August 2026. Check their status before you act: if upstream has shipped ESLint 10 support by the time you read this, the simplest path may be to upgrade the original plugin.
The fork described here targets a specific support matrix:
- React 19 and later
- ESLint 10 and later, flat config only
- Biome 2.5.8 and later
- Node.js 22.13, 24 and 26
The Biome-first setup this fork assumes
The rule selection only makes sense against a particular stack. Biome is the primary formatter and linter and covers general JavaScript, TypeScript, JSX, DOM and most React checks. ESLint stays in the toolchain only for what Biome does not provide: framework plugins and a handful of React 19 contracts.
The reference projects run:
- React 19 on Next.js 16, written in TypeScript
- Biome configured with the
allpreset - ESLint 10 using flat config
- Yarn 4 as the package manager
- Node.js 22, 24 and 26
In that arrangement you do not want a second, overlapping linter. You want only the React-specific checks that still add information after Biome has run.
From 102 active rules to 11
The fork starts from the upstream repository and keeps its Git history, MIT license and attribution; it is maintained independently from there. At the commit where it branched, upstream exported 104 rule modules. The all preset enabled 102 of them (the other two were deprecated), and recommended listed 22 rules, of which react/no-unsafe was explicitly turned off, leaving 21 enforced.
Porting everything would have kept the package large without giving it a clear purpose. Instead, each rule was sorted by the kind of decision it enforces:
- If Biome already reports the same diagnostic, the rule is dropped.
- If it is about formatting, naming, file layout or team policy, it belongs to Biome or the application's own config.
- If it is a broad heuristic that needs project-wide or type-aware evidence, it is dropped rather than left to report uncertain results.
- If it exists for React 18, legacy
.eslintrcconfigs, parser workarounds or obsolete React APIs, it falls outside the React 19 contract. - If it catches React 19 correctness issues or gives a useful React-specific performance warning, it stays or gets implemented.
Exactly 20 rules fell into the first bucket, including jsx-key, no-danger, no-unknown-property and self-closing-comp. A mapping document in the fork's docs folder pairs each of them with its Biome equivalent.
Rules that encode style or policy, for example prefer-stateless-function, jsx-sort-props or function-component-definition, were cut since they say nothing about React 19 correctness. no-unused-prop-types and no-unused-state went because a single-file AST check cannot reliably answer a question that spans the whole project; noisy rules train people, and agents, to ignore lint output. Everything else excluded was legacy compatibility code outside the stated support matrix.
Only four upstream rule IDs made it through: no-deprecated, no-invalid-html-attribute, no-direct-mutation-state and jsx-no-constructed-context-values.
New rules, and one that was deliberately dropped
Three unmerged upstream proposals were relevant to a React 19 cutover:
- flag components that render
undefined(upstream issue #3020) - forbid
defaultPropson function components (issue #3911) - prefer a lazy initializer for
useState(PR #3579)
All three were implemented, and then no-render-return-undefined was removed again. React 19 allows a component to return undefined, so banning it would be a house style disguised as a framework rule. The other two shipped as no-function-default-props, which flags an API that React 19 ignores on function components, and prefer-use-state-lazy-initialization, a warning about avoidable work on every render, such as passing expensive() instead of () => expensive().
Five further rules target specific React 19 behavior, either new or tightened from upstream versions: no-prop-types, no-misspelled-lifecycle-methods, jsx-no-key-after-spread, controlled-form-requires-handler and no-implicit-ref-callback-return. The ref-callback rule is a good example of why this matters now: since React 19 lets a ref callback return a cleanup function, an arrow function that implicitly returns a value from a ref callback is no longer harmless.
Version 8.0.0 therefore ships 11 rules, all in recommended. The nine correctness rules are errors; the two performance rules are warnings. An all preset would either duplicate recommended or differ only in severity, so the package does not offer one.
What real projects caught that tests did not
By 8.0.0-rc.3 the unit tests and package checks passed. Installing the plugin in real applications is where the useful failures began.
The first class was HTML attribute metadata. no-invalid-html-attribute rejected perfectly valid attributes, including alt, accept, name, loading, form, and value on <select>, <option> and <textarea>. Getting it right took three issue-and-PR rounds in the fork's tracker (#21/#22, #25/#26 and #29/#31). The lasting fix was to treat WHATWG HTML content attributes and React DOM properties as two separate sources of truth instead of assuming one metadata table describes both.
The second failure came from Next.js. eslint-config-next@16 produces flat config, yet it reads rules from the legacy-shaped react.configs.recommended.rules field. The fork only exposed react.configs.flat.recommended, so configuration blew up before a single file was linted. A follow-up change (issue #24, PR #27) added that field for reading only, without bringing .eslintrc support back. Because Next.js imports the plugin by its unscoped name, a Yarn resolution is also needed, shown below.
Those integrations produced release candidates 4 through 6 and reshaped the test strategy. Before the final release, the packed npm tarball is linted with publint, imported by test consumers written in ESM, CommonJS and TypeScript, and run through the Next.js configuration shape the package claims to support, with CI on Node.js 22.13, 24 and 26. The lesson generalizes to any tooling package: test the artifact you publish, from the consumers you support, not just the source tree.
The resulting package is native ESM, flat-config only, and keeps the familiar react/* rule namespace.
Installing and configuring it
The commands use Yarn 4. First add Biome, ESLint 10 and the plugin as dev dependencies:
yarn add --dev @biomejs/biome@'>=2.5.8' eslint@^10 @ternaus/eslint-plugin-react@^8.0.0
Turn on Biome's full stable rule set and its React domain in biome.json, so Biome covers everything the fork intentionally left out:
{
"linter": {
"domains": {
"react": "all"
},
"rules": {
"preset": "all"
}
}
}
Then add the remaining React rules to eslint.config.js. The spread merges the preset's plugin registration and rules into a config object scoped by the files glob:
import react from '@ternaus/eslint-plugin-react';
export default [
{
files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],
...react.configs.flat.recommended,
},
];
If that glob includes .ts or .tsx files, register a TypeScript-capable parser in an earlier config object; the preset does not configure one for you.
Run the two tools side by side, typically as separate CI steps or one script:
yarn biome check .
yarn eslint .
Rule IDs keep the react prefix, so overrides written for the original plugin carry over for the rules that still exist:
{
rules: {
'react/no-deprecated': 'error',
'react/no-implicit-ref-callback-return': 'error',
},
}
Wiring it into Next.js
eslint-config-next imports the plugin as eslint-plugin-react. With Yarn you can redirect that name to the fork through resolutions:
{
"devDependencies": {
"@ternaus/eslint-plugin-react": "8.0.0"
},
"resolutions": {
"eslint-plugin-react": "npm:@ternaus/eslint-plugin-react@8.0.0"
}
}
Keep the two version numbers in lockstep. The resolution stops eslint-config-next from pulling in the ESLint 9-only original next to the ESLint 10 fork. Since Next.js registers the plugin under react, existing react/* rule IDs keep working.
When this fork is the wrong choice
- It does not include every upstream rule. Configs that depend on
react/prop-types,react/display-nameorreact/jsx-sort-propsshould compare against the fork's list of supported rules in its repository before switching. - It assumes Biome covers the rest. Without Biome, or an equivalent, dropping to 11 rules leaves real gaps such as missing
keyprops. - It offers no React Native compatibility contract, and its DOM rules only analyze elements proven to be lowercase HTML tags.
- It is an independently maintained fork. Weigh that against waiting for upstream support, and revisit the choice once the upstream pull request lands.
Key takeaways
- The ESLint 10 crash comes from removed rule-context APIs, so only a plugin release fixes it.
- A Biome-first stack needs far fewer ESLint React rules; triaging rules by the kind of decision they enforce is a reusable method for trimming any overlapping lint setup.
- Rules that need whole-project evidence produce noise in a per-file linter and are better removed than tolerated.
- Test published tooling as consumers see it: the packed archive, every module format, and real framework configs such as
eslint-config-next. - With Next.js, a Yarn
resolutionsalias lets the fork stand in for the unscoped package without changing rule IDs.
The source and release notes live in the fork's repository.