diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index b200941cf1d1c7..918918f0ce2981 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -217,6 +217,7 @@ mod react { pub mod no_find_dom_node; pub mod no_is_mounted; pub mod no_render_return_value; + pub mod no_set_state; pub mod no_string_refs; pub mod no_unescaped_entities; pub mod no_unknown_property; @@ -687,6 +688,7 @@ oxc_macros::declare_all_lint_rules! { react::no_direct_mutation_state, react::no_find_dom_node, react::no_render_return_value, + react::no_set_state, react::no_string_refs, react::no_unescaped_entities, react::no_is_mounted, diff --git a/crates/oxc_linter/src/rules/react/no_set_state.rs b/crates/oxc_linter/src/rules/react/no_set_state.rs new file mode 100644 index 00000000000000..e9d660a62f8d8f --- /dev/null +++ b/crates/oxc_linter/src/rules/react/no_set_state.rs @@ -0,0 +1,165 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{get_parent_es5_component, get_parent_es6_component}, + AstNode, +}; + +fn no_set_state_diagnostic(span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("eslint-plugin-react(no-set-state): Do not use setState").with_label(span0) +} + +#[derive(Debug, Default, Clone)] +pub struct NoSetState; + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow the usage of `this.setState` in React components. + /// + /// ### Why is this bad? + /// + /// When using an architecture that separates your application state from your UI components + /// (e.g. Flux), it may be desirable to forbid the use of local component state. This rule is + /// especially helpful in read-only applications (that don't use forms), since local component + /// state should rarely be necessary in such cases. + /// + /// ### Example + /// ```javascript + /// var Hello = createReactClass({ + /// getInitialState: function() { + /// return { + /// name: this.props.name + /// }; + /// }, + /// handleClick: function() { + /// this.setState({ + /// name: this.props.name.toUpperCase() + /// }); + /// }, + /// render: function() { + /// return
Hello {this.state.name}
; + /// } + /// }); + /// ``` + NoSetState, + style, +); + +impl Rule for NoSetState { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + let Some(member_expr) = call_expr.callee.as_member_expression() else { + return; + }; + + if !matches!(member_expr.object(), Expression::ThisExpression(_)) + || !member_expr.static_property_name().is_some_and(|str| str == "setState") + || !(get_parent_es5_component(node, ctx).is_some() + || get_parent_es6_component(ctx).is_some()) + { + return; + } + + ctx.diagnostic(no_set_state_diagnostic(call_expr.callee.span())); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + " + var Hello = function() { + this.setState({}) + }; + ", + " + var Hello = createReactClass({ + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + " + var Hello = createReactClass({ + componentDidUpdate: function() { + someNonMemberFunction(arg); + this.someHandler = this.setState; + }, + render: function() { + return
Hello {this.props.name}
; + } + }); + ", + ]; + + let fail = vec![ + " + var Hello = createReactClass({ + componentDidUpdate: function() { + this.setState({ + name: this.props.name.toUpperCase() + }); + }, + render: function() { + return
Hello {this.state.name}
; + } + }); + ", + " + var Hello = createReactClass({ + someMethod: function() { + this.setState({ + name: this.props.name.toUpperCase() + }); + }, + render: function() { + return
Hello {this.state.name}
; + } + }); + ", + " + class Hello extends React.Component { + someMethod() { + this.setState({ + name: this.props.name.toUpperCase() + }); + } + render() { + return
Hello {this.state.name}
; + } + }; + ", + " + class Hello extends React.Component { + someMethod = () => { + this.setState({ + name: this.props.name.toUpperCase() + }); + } + render() { + return
Hello {this.state.name}
; + } + }; + ", + " + class Hello extends React.Component { + render() { + return
this.setState({dropdownIndex: index})} />; + } + }; + ", + ]; + + Tester::new(NoSetState::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_set_state.snap.new b/crates/oxc_linter/src/snapshots/no_set_state.snap.new new file mode 100644 index 00000000000000..05bc3f6bb05143 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_set_state.snap.new @@ -0,0 +1,43 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 216 +--- + ⚠ eslint-plugin-react(no-set-state): Do not use setState + ╭─[no_set_state.tsx:4:16] + 3 │ componentDidUpdate: function() { + 4 │ this.setState({ + · ───────────── + 5 │ name: this.props.name.toUpperCase() + ╰──── + + ⚠ eslint-plugin-react(no-set-state): Do not use setState + ╭─[no_set_state.tsx:4:16] + 3 │ someMethod: function() { + 4 │ this.setState({ + · ───────────── + 5 │ name: this.props.name.toUpperCase() + ╰──── + + ⚠ eslint-plugin-react(no-set-state): Do not use setState + ╭─[no_set_state.tsx:4:16] + 3 │ someMethod() { + 4 │ this.setState({ + · ───────────── + 5 │ name: this.props.name.toUpperCase() + ╰──── + + ⚠ eslint-plugin-react(no-set-state): Do not use setState + ╭─[no_set_state.tsx:4:16] + 3 │ someMethod = () => { + 4 │ this.setState({ + · ───────────── + 5 │ name: this.props.name.toUpperCase() + ╰──── + + ⚠ eslint-plugin-react(no-set-state): Do not use setState + ╭─[no_set_state.tsx:4:48] + 3 │ render() { + 4 │ return
this.setState({dropdownIndex: index})} />; + · ───────────── + 5 │ } + ╰────