{"slug": "more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10", "title": "More Agent Autonomy Needs Stronger Guardrails: React 19 Linting on ESLint 10", "summary": "A developer released @ternaus/eslint-plugin-react, an independent React 19 continuation for ESLint 10, after the upstream eslint-plugin-react failed to support the new ESLint version. The plugin narrows the rule set to React 19-specific checks that complement Biome, removing rules that duplicate Biome diagnostics or express style preferences. The developer's projects, including Albumentations.ai, sportscategory.info, and my-roots.me, use the plugin as part of stronger guardrails for coding agents.", "body_md": "The more autonomy I give coding agents, the more of a repository's expectations need to be executable. I do not want manual review to be the first place a predictable failure is discovered.\n\nMy projects now have more pre-commit hooks, tests, deterministic checks for project conventions, and small reviewable commits. The checks turn expectations into pass/fail results an agent can act on; the commits keep failures narrow enough to diagnose.\n\nLinting is one of those guardrails. This story began when the React linting layer I relied on broke during an ESLint 10 upgrade.\n\nI maintain several React applications and wanted to upgrade them to ESLint 10. The upgrade stopped at `eslint-plugin-react`\n\n.\n\nESLint 10 [removed deprecated rule-context APIs](https://eslint.org/blog/2026/02/eslint-v10.0.0-released/). The current `eslint-plugin-react@7.37.5`\n\nrelease declares support only through ESLint 9 and can crash under ESLint 10 with:\n\n```\nTypeError: contextOrFilename.getFilename is not a function\n```\n\nThe upstream [compatibility issue](https://github.com/jsx-eslint/eslint-plugin-react/issues/3977) was opened on February 7, 2026. As of August 23, it is still open more than six months later. The [pull request](https://github.com/jsx-eslint/eslint-plugin-react/pull/4022), opened on July 30, is also still open.\n\nThat led me to release [ @ternaus/eslint-plugin-react](https://github.com/ternaus/eslint-plugin-react): an independent React 19 continuation for ESLint 10.\n\nThe package has a deliberately narrow support matrix:\n\n| Tool | Supported version |\n|---|---|\n| React | 19+ |\n| ESLint | 10 |\n| Biome | 2.5.8+ |\n| Node.js | 22.13, 24, or 26 |\n| ESLint config | Flat config |\n\nReact 18, ESLint 9, and `.eslintrc*`\n\nare outside the package contract.\n\nMy projects use Biome as the primary formatter and linter. Biome handles general JavaScript, TypeScript, JSX, DOM, and most React checks.\n\nESLint remains for checks that Biome does not provide, including framework plugins and several React 19 contracts.\n\nThe setup came from real Next.js and React codebases behind [Albumentations.ai](https://albumentations.ai), [sportscategory.info](https://sportscategory.info), and [my-roots.me](https://my-roots.me):\n\n`all`\n\npresetMy projects need a smaller rule set: the React-specific checks that still add information after Biome has finished.\n\nI started from the upstream repository while preserving its Git history, MIT license, and attribution. The project is now maintained independently.\n\nAt the commit where the fork began, the upstream plugin exported 104 rule modules. Its `all`\n\npreset enabled 102 active rules; the other two were deprecated. The `recommended`\n\npreset named 22 rules, although one of them, `react/no-unsafe`\n\n, was explicitly disabled, so it enforced 21.\n\nCarrying all of that forward would have preserved the package size without preserving a clear purpose. My target was narrower: React 19 checks that still add information after Biome has run.\n\nI reviewed the rules by the kind of decision they enforce:\n\n| Group | Decision |\n|---|---|\n| Same diagnostic already available in Biome | Remove from this plugin |\n| Formatting, naming, file layout, or team policy | Leave to Biome or the application |\n| Broad heuristics that need project-wide or type-aware evidence | Remove rather than report uncertain results |\n| React 18, classic config, parser workarounds, or obsolete React APIs | Remove from the React 19 contract |\n| React 19 correctness or a useful React-specific performance warning | Keep or implement |\n\nThe first group accounted for exactly 20 rules, including `jsx-key`\n\n, `no-danger`\n\n, `no-unknown-property`\n\n, and `self-closing-comp`\n\n. The [upstream rule support policy](https://github.com/ternaus/eslint-plugin-react/blob/main/docs/upstream-rule-support.md) records the complete one-to-one mapping to Biome.\n\nRules such as `jsx-sort-props`\n\n, `function-component-definition`\n\n, and `prefer-stateless-function`\n\nwere removed because they express style or team policy, not React 19 correctness. Rules such as `no-unused-prop-types`\n\nand `no-unused-state`\n\nwere removed because a local AST heuristic cannot reliably answer a project-wide question. The remaining exclusions were legacy compatibility surface outside the package's stated support matrix.\n\nFour original rule IDs survived that review: `jsx-no-constructed-context-values`\n\n, `no-deprecated`\n\n, `no-direct-mutation-state`\n\n, and `no-invalid-html-attribute`\n\n.\n\nAmong the upstream proposals relevant to this React 19 cutover, I evaluated three rule ideas that were still unmerged:\n\n`undefined`\n\n`defaultProps`\n\non function components`useState`\n\ninitializationI initially implemented all three. I then removed `no-render-return-undefined`\n\n: React 19 permits an `undefined`\n\nreturn, so forbidding it would be a team convention presented as a framework requirement. The other two became `no-function-default-props`\n\nand `prefer-use-state-lazy-initialization`\n\n. The first catches an API React 19 ignores; the second is a warning for avoidable render-time work.\n\nI also added or narrowed five rules around concrete React 19 behavior: `controlled-form-requires-handler`\n\n, `jsx-no-key-after-spread`\n\n, `no-implicit-ref-callback-return`\n\n, `no-misspelled-lifecycle-methods`\n\n, and `no-prop-types`\n\n.\n\nThat left 11 rules in version 8.0.0. All 11 are in `recommended`\n\n: nine correctness rules are errors, while the two performance rules are warnings. A separate `all`\n\npreset would either duplicate `recommended`\n\nor differ only by severity, so the package does not expose one.\n\nBy `8.0.0-rc.3`\n\n, the unit tests and package checks were green. Then I installed the plugin in Rooted and Albumentations.ai. That is where the useful failures started.\n\nThe first class of failures was HTML attribute metadata. `no-invalid-html-attribute`\n\nrejected valid attributes such as `alt`\n\n, `accept`\n\n, `name`\n\n, `loading`\n\n, `form`\n\n, and `value`\n\non `<select>`\n\n, `<option>`\n\n, and `<textarea>`\n\n. The fixes arrived in three rounds: [issue #21](https://github.com/ternaus/eslint-plugin-react/issues/21) and [PR #22](https://github.com/ternaus/eslint-plugin-react/pull/22), then [issue #25](https://github.com/ternaus/eslint-plugin-react/issues/25) and [PR #26](https://github.com/ternaus/eslint-plugin-react/pull/26), and finally [issue #29](https://github.com/ternaus/eslint-plugin-react/issues/29) and [PR #31](https://github.com/ternaus/eslint-plugin-react/pull/31). The final fix separated WHATWG HTML content attributes from React DOM properties instead of treating one metadata source as the whole contract.\n\nThe second failure came from Next.js. `eslint-config-next@16`\n\nbuilds flat config, but it still reads rules from the legacy-shaped `react.configs.recommended.rules`\n\nfield. My package exposed only `react.configs.flat.recommended`\n\n, so configuration failed before ESLint could lint a file. [Issue #24](https://github.com/ternaus/eslint-plugin-react/issues/24) and [PR #27](https://github.com/ternaus/eslint-plugin-react/pull/27) added that read-compatible field without bringing `.eslintrc`\n\nsupport back into scope. I also documented the Yarn resolution needed because Next.js imports the plugin under the unscoped `eslint-plugin-react`\n\nname.\n\nThose integrations produced release candidates 4, 5, and 6. They also changed the test strategy: the final release checks the packed npm archive with `publint`\n\n, loads it from ESM, CommonJS, and TypeScript consumers, and exercises the supported Next.js configuration shape. CI covers Node.js 22.13, 24, and 26.\n\nThe resulting package uses native ESM, ESLint flat config, and the familiar `react/*`\n\nrule namespace.\n\nThe examples below use Yarn 4.\n\nInstall Biome, ESLint 10, and the plugin:\n\n```\nyarn add --dev @biomejs/biome@'>=2.5.8' eslint@^10 @ternaus/eslint-plugin-react@^8.0.0\n```\n\nEnable Biome's full stable rule set and React domain:\n\n```\n{\n  \"linter\": {\n    \"domains\": {\n      \"react\": \"all\"\n    },\n    \"rules\": {\n      \"preset\": \"all\"\n    }\n  }\n}\n```\n\nThen add the residual React rules to `eslint.config.js`\n\n:\n\n``` python\nimport react from '@ternaus/eslint-plugin-react';\n\nexport default [\n  {\n    files: ['**/*.{js,jsx,mjs,cjs,ts,tsx}'],\n    ...react.configs.flat.recommended,\n  },\n];\n```\n\nIf the glob includes TypeScript, configure a parser that supports TypeScript earlier in the flat config.\n\nRun both tools:\n\n```\nyarn biome check .\nyarn eslint .\n```\n\nRule IDs remain under the `react`\n\nnamespace:\n\n```\n{\n  rules: {\n    'react/no-deprecated': 'error',\n    'react/no-implicit-ref-callback-return': 'error',\n  },\n}\n```\n\n`eslint-config-next`\n\nimports the package under the unscoped name `eslint-plugin-react`\n\n. With Yarn, map that dependency to the maintained package:\n\n```\n{\n  \"devDependencies\": {\n    \"@ternaus/eslint-plugin-react\": \"8.0.0\"\n  },\n  \"resolutions\": {\n    \"eslint-plugin-react\": \"npm:@ternaus/eslint-plugin-react@8.0.0\"\n  }\n}\n```\n\nKeep both versions synchronized. This prevents `eslint-config-next`\n\nfrom installing the ESLint 9-only package alongside the ESLint 10 continuation.\n\nBecause Next.js registers it under `react`\n\n, existing `react/*`\n\nrule IDs continue to work.\n\nThis package does not cover every rule in the original plugin.\n\nIf your configuration depends on rules such as `react/jsx-sort-props`\n\n, `react/display-name`\n\n, or `react/prop-types`\n\n, check the [complete rule catalog](https://github.com/ternaus/eslint-plugin-react/blob/main/docs/rules/README.md) before migrating.\n\nThe package also has no React Native-specific compatibility contract. DOM rules only analyze proven lowercase HTML elements.", "url": "https://wpnews.pro/news/more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10", "canonical_source": "https://dev.to/viglovikov/i-needed-react-linting-on-eslint-10-so-i-built-a-react-19-continuation-39id", "published_at": "2026-08-23 05:59:08+00:00", "updated_at": "2026-08-23 06:43:22.722524+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["@ternaus/eslint-plugin-react", "eslint-plugin-react", "ESLint 10", "React 19", "Biome", "Albumentations.ai", "sportscategory.info", "my-roots.me"], "alternates": {"html": "https://wpnews.pro/news/more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10", "markdown": "https://wpnews.pro/news/more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10.md", "text": "https://wpnews.pro/news/more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10.txt", "jsonld": "https://wpnews.pro/news/more-agent-autonomy-needs-stronger-guardrails-react-19-linting-on-eslint-10.jsonld"}}