Skip to content

feat(linter): Implement react/prefer-function-component#19652

Merged
camc314 merged 14 commits intomainfrom
react-prefer-func-comp
Mar 30, 2026
Merged

feat(linter): Implement react/prefer-function-component#19652
camc314 merged 14 commits intomainfrom
react-prefer-func-comp

Conversation

@connorshea
Copy link
Copy Markdown
Member

@connorshea connorshea commented Feb 24, 2026

This is based on the rule provided in eslint-plugin-react-prefer-function-component. This rule is proposed for eslint-plugin-react, but so far not merged.

The goal of the rule is pretty simple. We want to discourage class components like this, as they are not used very often in modern React:

class Foo extends React.Component {
  render() {
    return <div>{this.props.foo}</div>;
  }
}

class Bar extends React.PureComponent {
  render() {
    return <div>{this.props.bar}</div>;
  }
}

And encourage function components like this:

const Foo = function(props) {
  return <div>{props.foo}</div>;
};

const Bar = ({ bar }) => <div>{bar}</div>;

The original eslint-plugin-react plugin lacks a lint rule like this, which is very silly as it has one for preferring class components (which are notably discouraged by React itself nowadays). It does have prefer-stateless-functions, however I would strongly recommend we mark that rule as unsupported in favor of this rule. If you look at the tests for prefer-stateless-function, you can see how lenient it is in allowing class components (anything with a method other than render() is allowed, basically). And it has the usual eslint-plugin-react smells of supporting things from many many many years ago. This rule is much more straight-forward, and generally a better implementation of the concept.

We could, alternatively, implement this based on this rule from @eslint-react/eslint-plugin (different from eslint-plugin-react), however I generally prefer the prefer-function-component name and the @eslint-react rule was generally going to be more complex to implement.

AI Disclosure: Generated with Claude Code w/ Opus 4.5, after prep work by me. Tested and reviewed by me. I created the rule via the rulegen tooling, then manually copied over the initial set of test cases myself. I then had Claude create the remaining tests from the original source code. From looking through the tests, I am confident in the quality/accuracy of the ported tests, and will be doing further testing via oxc-ecosystem-ci.

Ecosystem CI run: https://github.com/oxc-project/oxc-ecosystem-ci/actions/runs/22334972119

Bsky for example.

main:

Found 0 warnings and 78356 errors.
Finished in 1.7s on 1553 files with 629 rules using 4 threads.

this PR:

Found 0 warnings and 78361 errors.
Finished in 1.5s on 1553 files with 630 rules using 4 threads.

The 5 extra violations are due to this rule, and all 5 are correctly catching class component usage. In terms of performance, there's clearly no noticeable difference here, even on a React codebase of a decent size.

@github-actions github-actions bot added A-linter Area - Linter C-enhancement Category - New feature or request labels Feb 24, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Feb 24, 2026

Merging this PR will not alter performance

✅ 4 untouched benchmarks
⏩ 52 skipped benchmarks1


Comparing react-prefer-func-comp (296000d) with main (871f9d9)

Open in CodSpeed

Footnotes

  1. 52 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@connorshea connorshea marked this pull request as ready for review February 24, 2026 03:24
@connorshea connorshea requested a review from camc314 as a code owner February 24, 2026 03:24
Copilot AI review requested due to automatic review settings February 24, 2026 03:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new React lint rule to discourage class components in favor of function components, based on eslint-plugin-react-prefer-function-component, and wires it into the linter’s rule registry and snapshots.

Changes:

  • Implement react/prefer-function-component with configurable allowances for error boundaries and JSX utility classes.
  • Add comprehensive rule tests and a snapshot for expected diagnostics.
  • Register the new rule in the React module list and generated rule enum/runner plumbing.

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/oxc_linter/src/rules/react/prefer_function_component.rs New rule implementation, config, and test coverage/snapshot generation.
crates/oxc_linter/src/snapshots/react_prefer_function_component.snap Snapshot output for the new rule’s diagnostics.
crates/oxc_linter/src/rules.rs Exposes the new React rule module.
crates/oxc_linter/src/generated/rules_enum.rs Adds the rule to the generated enum, IDs, and metadata dispatch.
crates/oxc_linter/src/generated/rule_runner_impls.rs Registers the rule runner node type (Class) for execution.

@connorshea
Copy link
Copy Markdown
Member Author

Tested on the bsky codebase and confirmed that all of these are correct violations that should indeed be showing up:

  × eslint-plugin-react(prefer-function-component): Class component should be written as a function component.
    ╭─[modules/bottom-sheet/src/BottomSheetNativeComponent.tsx:40:14]
 39 │ 
 40 │ export class BottomSheetNativeComponent extends React.Component<
    ·              ──────────────────────────
 41 │   BottomSheetViewProps,
    ╰────
  help: Convert the class component to a function component.
  note: See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function

  × eslint-plugin-react(prefer-function-component): Class component should be written as a function component.
    ╭─[__mocks__/react-native-svg.js:7:10]
  6 │     const createComponent = function (name) {
  7 │ ╭─▶   return class extends React.Component {
  8 │ │       // overwrite the displayName, since this is a class created dynamically
  9 │ │       static displayName = name
 10 │ │   
 11 │ │       render() {
 12 │ │         return React.createElement(name, this.props, this.props.children)
 13 │ │       }
 14 │ ╰─▶   }
 15 │     }
    ╰────
  help: Convert the class component to a function component.
  note: See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function

  × eslint-plugin-react(prefer-function-component): Class component should be written as a function component.
    ╭─[__mocks__/@gorhom/bottom-sheet.tsx:10:7]
  9 │ }
 10 │ class BottomSheet extends React.Component<{
    ·       ───────────
 11 │   onClose?: () => void
    ╰────
  help: Convert the class component to a function component.
  note: See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function

  × eslint-plugin-react(prefer-function-component): Class component should be written as a function component.
   ╭─[modules/expo-bluesky-gif-view/src/GifView.web.tsx:6:14]
 5 │ 
 6 │ export class GifView extends React.PureComponent<GifViewProps> {
   ·              ───────
 7 │   private readonly videoPlayerRef: React.RefObject<HTMLMediaElement> =
   ╰────
  help: Convert the class component to a function component.
  note: See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function

  × eslint-plugin-react(prefer-function-component): Class component should be written as a function component.
    ╭─[modules/expo-bluesky-gif-view/src/GifView.tsx:12:14]
 11 │ 
 12 │ export class GifView extends React.PureComponent<GifViewProps> {
    ·              ───────
 13 │   // TODO native types, should all be the same as those in this class
    ╰────
  help: Convert the class component to a function component.
  note: See https://react.dev/reference/react/Component#migrating-a-simple-component-from-a-class-to-a-function

Found 0 warnings and 5 errors.
Finished in 75ms on 1553 files with 1 rules using 8 threads.

@camc314 camc314 self-assigned this Feb 27, 2026
@camc314 camc314 force-pushed the react-prefer-func-comp branch from 5770234 to 108bec3 Compare March 30, 2026 20:12
Copy link
Copy Markdown
Contributor

@camc314 camc314 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💪 LGTM - thanks!

@camc314 camc314 changed the title feat(linter): Implement react/prefer-function-component feat(linter): Implement react/prefer-function-component Mar 30, 2026
@camc314 camc314 merged commit 98510d2 into main Mar 30, 2026
26 checks passed
@camc314 camc314 deleted the react-prefer-func-comp branch March 30, 2026 20:18
leaysgur pushed a commit that referenced this pull request Apr 7, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 22ce6af oxlint/lsp: [**BREAKING**] Show/fix safe suggestions by
default (#19816) (Sysix)

### 🚀 Features

- 7a7b7b8 oxlint/lsp: Add source.fixAllDangerous.oxc code action kind
(#20526) (bab)
- 9cfe57e linter/unicorn: Implement prefer-import-meta-properties rule
(#20662) (Irfan - ئىرفان)
- 1edb391 linter/eslint: Implement `no-restricted-exports` rule (#20592)
(Nicolas Le Cam)
- 0f12bcd linter/react: Implement `hook-use-state` rule (#20986) (Khaled
Labeb)
- 1513a9f oxlint/lsp: Show note field for lsp diagnostic (#20983)
(Sysix)
- 7fdf722 linter/unicorn: Implement `no-useless-iterator-to-array` rule
(#20945) (Mikhail Baev)
- 39c8f2c linter/jest: Implement padding-around-after-all-blocks
(#21034) (Sapphire)
- ac39e51 linter/eslint-vitest-plugin: Prefer importing vitest globals
(#20960) (Said Atrahouch)
- 0b84de1 oxlint: Support allow option for prefer-promise-reject-errors
(#20934) (camc314)
- 23db851 linter/consistent-return: Move rule from nursery to suspicious
(#20920) (camc314)
- 9a27e32 linter/no-unnecessary-type-conversion: Move rule from nursery
to suspicious (#20919) (camc314)
- 1ca7b58 linter/dot-notation: Move rule from nursery to style (#20918)
(camc314)
- 73ba81a linter/consistent-type-exports: Move rule from nursery to
style (#20917) (camc314)
- b9199b1 linter/unicorn: Implement switch-case-break-position (#20872)
(Mikhail Baev)
- 3435ff8 linter: Implements `prefer-snapshot-hint` rule in Jest and
Vitest (#20870) (Said Atrahouch)
- 98510d2 linter: Implement react/prefer-function-component (#19652)
(Connor Shea)
- 871f9d9 linter: Implement no-useless-assignment (#15466) (Zhaoting
Zhou)
- 0f01fbd linter: Implement eslint/object-shorthand (#17688) (yue)

### 🐛 Bug Fixes

- dd2df87 npm: Export package.json for oxlint and oxfmt (#20784) (kazuya
kawaguchi)
- 9bc77dd linter/no-unused-private-class-members: False positive with
await expr (#21067) (camc314)
- 60a57cd linter/const-comparisons: Detect equality contradictions
(#21065) (camc314)
- 2bb2be2 linter/no-array-index-key: False positive when index is passed
as function argument (#21012) (bab)
- 6492953 linter/no-this-in-sfc: Only flag `this` used as member
expression object (#20961) (bab)
- 9446dcc oxlint/lsp: Skip `node_modules` in oxlint config walker
(#21004) (copilot-swe-agent)
- af89923 linter/no-namespace: Support glob pattern matching against
basename (#21031) (bab)
- 64a1a7e oxlint: Don't search for nested config outside base config
(#21051) (Sysix)
- 3b953bc linter/button-has-type: Ignore `document.createElement` calls
(#21008) (Said Atrahouch)
- 8c36070 linter/unicorn: Add support for `Array.from()` for
`prefer-set-size` rule (#21016) (Mikhail Baev)
- c1a48f0 linter: Detect vitest import from vite-plus/test (#20976)
(Said Atrahouch)
- 5c32fd1 lsp: Prevent corrupted autofix output from overlapping text
edits (#19793) (Peter Wagenet)
- ca79960 linter/no-array-index-key: Move span to `key` property
(#20947) (camc314)
- 2098274 linter: Add suggestion for `jest/prefer-equality-matcher`
(#20925) (eryue0220)
- 6eb77ec linter: Allow default-import barrels in import/named (#20757)
(Bazyli Brzóska)
- 9c218ef linter/eslint-vitest-plugin: Remove pending fix status for
require-local-test-context-for-concurrent-snapshot (#20890) (Said
Atrahouch)

### ⚡ Performance

- fb52383 napi/parser, linter/plugins: Clear buffers and source texts
earlier (#21025) (overlookmotel)
- 3b7dec4 napi/parser, linter/plugins: Use `utf8Slice` for decoding
UTF-8 strings (#21022) (overlookmotel)
- 012c924 napi/parser, linter/plugins: Speed up decoding strings in raw
transfer (#21021) (overlookmotel)
- 55e1e9b napi/parser, linter/plugins: Initialize vars as 0 (#21020)
(overlookmotel)
- c25ef02 napi/parser, linter/plugins: Simplify branch condition in
`deserializeStr` (#21019) (overlookmotel)
- 9f494c3 napi/parser, linter/plugins: Raw transfer use
`String.fromCharCode` in string decoding (#21018) (overlookmotel)
- 0503a78 napi/parser, linter/plugins: Faster deserialization of `raw`
fields (#20923) (overlookmotel)
- a24f75e napi/parser: Optimize string deserialization for non-ASCII
sources (#20834) (Joshua Tuddenham)

### 📚 Documentation

- af72b80 oxlint: Fix typo for --tsconfig (#20889) (leaysgur)
- 70c53b1 linter: Highlight that tsconfig is not respected in type aware
linting (#20884) (camc314)
# Oxfmt
### 🚀 Features

- 35cf6e8 oxfmt: Add node version hint for ts config import failures
(#21046) (camc314)

### 🐛 Bug Fixes

- dd2df87 npm: Export package.json for oxlint and oxfmt (#20784) (kazuya
kawaguchi)
- 9d45511 oxfmt: Propagate file write errors instead of panicking
(#20997) (leaysgur)
- 139ddd9 formatter: Handle leading comment after array elision (#20987)
(leaysgur)
- 4216380 oxfmt: Support `.editorconfig` `tab_width` fallback (#20988)
(leaysgur)
- d10df39 formatter: Resolve pending space in fits measurer before
expanded-mode early exit (#20954) (Dunqing)
- f9ef1bd formatter: Avoid breaking after `=>` when arrow body has JSDoc
type cast (#20857) (bab)

Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-linter Area - Linter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants