diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index a6d7156367a4c..109e93ddfdc8e 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -218,6 +218,7 @@ mod react {
pub mod no_string_refs;
pub mod no_unescaped_entities;
pub mod no_unknown_property;
+ pub mod prefer_es6_class;
pub mod react_in_jsx_scope;
pub mod require_render_return;
pub mod rules_of_hooks;
@@ -686,6 +687,7 @@ oxc_macros::declare_all_lint_rules! {
react::no_unescaped_entities,
react::no_is_mounted,
react::no_unknown_property,
+ react::prefer_es6_class,
react::require_render_return,
react::rules_of_hooks,
react::void_dom_elements_no_children,
diff --git a/crates/oxc_linter/src/rules/react/prefer_es6_class.rs b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs
new file mode 100644
index 0000000000000..bcaa346459f40
--- /dev/null
+++ b/crates/oxc_linter/src/rules/react/prefer_es6_class.rs
@@ -0,0 +1,188 @@
+use oxc_ast::AstKind;
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{GetSpan, Span};
+
+use crate::{
+ context::LintContext,
+ rule::Rule,
+ utils::{is_es5_component, is_es6_component},
+ AstNode,
+};
+
+fn unexpected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic {
+ OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Components should use createClass instead of ES6 class.")
+ .with_label(span0)
+}
+
+fn expected_es6_class_diagnostic(span0: Span) -> OxcDiagnostic {
+ OxcDiagnostic::warn("eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass.")
+ .with_label(span0)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct PreferEs6Class {
+ prefer_es6_class_option: PreferES6ClassOptionType,
+}
+
+declare_oxc_lint!(
+ /// ### What it does
+ ///
+ /// React offers you two ways to create traditional components: using the ES5
+ /// create-react-class module or the new ES6 class system.
+ ///
+ /// ### Why is this bad?
+ ///
+ /// This rule enforces a consistent React class style.
+ ///
+ /// ### Example
+ /// ```javascript
+ /// var Hello = createReactClass({
+ /// render: function() {
+ /// return
Hello {this.props.name}
;
+ /// }
+ /// });
+ /// ```
+ PreferEs6Class,
+ style,
+);
+
+impl Rule for PreferEs6Class {
+ fn from_configuration(value: serde_json::Value) -> Self {
+ let obj = value.get(0);
+
+ Self {
+ prefer_es6_class_option: obj
+ .and_then(serde_json::Value::as_str)
+ .map(PreferES6ClassOptionType::from)
+ .unwrap_or_default(),
+ }
+ }
+
+ fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+ if matches!(self.prefer_es6_class_option, PreferES6ClassOptionType::Always) {
+ if is_es5_component(node) {
+ let AstKind::CallExpression(call_expr) = node.kind() else {
+ return;
+ };
+ ctx.diagnostic(expected_es6_class_diagnostic(call_expr.callee.span()));
+ }
+ } else if is_es6_component(node) {
+ let AstKind::Class(class_expr) = node.kind() else {
+ return;
+ };
+ ctx.diagnostic(unexpected_es6_class_diagnostic(
+ class_expr.id.as_ref().map_or(class_expr.span, |id| id.span),
+ ));
+ }
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+enum PreferES6ClassOptionType {
+ #[default]
+ Always,
+ Never,
+}
+
+impl PreferES6ClassOptionType {
+ pub fn from(raw: &str) -> Self {
+ match raw {
+ "always" => Self::Always,
+ _ => Self::Never,
+ }
+ }
+}
+
+#[test]
+fn test() {
+ use crate::tester::Tester;
+
+ let pass = vec![
+ (
+ r"
+ class Hello extends React.Component {
+ render() {
+ return Hello {this.props.name}
;
+ }
+ }
+ Hello.displayName = 'Hello'
+ ",
+ None,
+ ),
+ (
+ r"
+ export default class Hello extends React.Component {
+ render() {
+ return Hello {this.props.name}
;
+ }
+ }
+ Hello.displayName = 'Hello'
+ ",
+ None,
+ ),
+ (
+ r"
+ var Hello = 'foo';
+ module.exports = {};
+ ",
+ None,
+ ),
+ (
+ r"
+ var Hello = createReactClass({
+ render: function() {
+ return Hello {this.props.name}
;
+ }
+ });
+ ",
+ Some(serde_json::json!(["never"])),
+ ),
+ (
+ r"
+ class Hello extends React.Component {
+ render() {
+ return Hello {this.props.name}
;
+ }
+ }
+ ",
+ Some(serde_json::json!(["always"])),
+ ),
+ ];
+
+ let fail = vec![
+ (
+ r"
+ var Hello = createReactClass({
+ displayName: 'Hello',
+ render: function() {
+ return Hello {this.props.name}
;
+ }
+ });
+ ",
+ None,
+ ),
+ (
+ r"
+ var Hello = createReactClass({
+ render: function() {
+ return Hello {this.props.name}
;
+ }
+ });
+ ",
+ Some(serde_json::json!(["always"])),
+ ),
+ (
+ r"
+ class Hello extends React.Component {
+ render() {
+ return Hello {this.props.name}
;
+ }
+ }
+ ",
+ Some(serde_json::json!(["never"])),
+ ),
+ ];
+
+ Tester::new(PreferEs6Class::NAME, pass, fail).test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap
new file mode 100644
index 0000000000000..167561671b1ca
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/prefer_es_6_class.snap
@@ -0,0 +1,26 @@
+---
+source: crates/oxc_linter/src/tester.rs
+---
+ ⚠ eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass.
+ ╭─[prefer_es_6_class.tsx:2:25]
+ 1 │
+ 2 │ var Hello = createReactClass({
+ · ────────────────
+ 3 │ displayName: 'Hello',
+ ╰────
+
+ ⚠ eslint-plugin-react(prefer-es6-class): Components should use es6 class instead of createClass.
+ ╭─[prefer_es_6_class.tsx:2:25]
+ 1 │
+ 2 │ var Hello = createReactClass({
+ · ────────────────
+ 3 │ render: function() {
+ ╰────
+
+ ⚠ eslint-plugin-react(prefer-es6-class): Components should use createClass instead of ES6 class.
+ ╭─[prefer_es_6_class.tsx:2:19]
+ 1 │
+ 2 │ class Hello extends React.Component {
+ · ─────
+ 3 │ render() {
+ ╰────