[compiler] Setup RecommendedLatest preset (#34782)
Renames the `recommended` property on LintRule to `preset`, to allow exporting rules for different presets. For now the `Recommended` and `RecommendedLatest` presets are the same, but in the next PR I will enable more rules for the latest preset. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34782). * #34783 * __->__ #34782
lauren committed
Oct 8, 2025 at 15:45 UTC
3e1b34dc51dfc3cbada2cdd0ead5acee6998f444
5 files changed
+96
-53
compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts
+44
-30
@@ -669,6 +669,21 @@ export enum ErrorCategory {
669
FBT = 'FBT',
670
}
671
672
+export enum LintRulePreset {
673
+ /**
674
+ * Rules that are stable and included in the `recommended` preset.
675
+ */
676
+ Recommended = 'recommended',
677
+ /**
678
+ * Rules that are more experimental and only included in the `recommended-latest` preset.
679
+ */
680
+ RecommendedLatest = 'recommended-latest',
681
+ /**
682
+ * Rules that are disabled.
683
+ */
684
+ Off = 'off',
685
+}
686
+
687
export type LintRule = {
688
// Stores the category the rule corresponds to, used to filter errors when reporting
689
category: ErrorCategory;
@@ -689,15 +704,14 @@ export type LintRule = {
704
description: string;
705
706
/**
692
- * If true, this rule will automatically appear in the default, "recommended" ESLint
693
- * rule set. Otherwise it will be part of an `allRules` export that developers can
694
- * use to opt-in to showing output of all possible rules.
707
+ * Configures the preset in which the rule is enabled. If 'off', the rule will not be included in
708
+ * any preset.
709
*
710
* NOTE: not all validations are enabled by default! Setting this flag only affects
711
* whether a given rule is part of the recommended set. The corresponding validation
712
* also should be enabled by default if you want the error to actually show up!
713
*/
700
- recommended: boolean;
714
+ preset: LintRulePreset;
715
};
716
717
const RULE_NAME_PATTERN = /^[a-z]+(-[a-z]+)*$/;
@@ -720,7 +734,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
734
name: 'automatic-effect-dependencies',
735
description:
736
'Verifies that automatic effect dependencies are compiled if opted-in',
723
- recommended: false,
737
+ preset: LintRulePreset.Off,
738
};
739
}
740
case ErrorCategory.CapitalizedCalls: {
@@ -730,7 +744,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
744
name: 'capitalized-calls',
745
description:
746
'Validates against calling capitalized functions/methods instead of using JSX',
733
- recommended: false,
747
+ preset: LintRulePreset.Off,
748
};
749
}
750
case ErrorCategory.Config: {
@@ -739,7 +753,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
753
severity: ErrorSeverity.Error,
754
name: 'config',
755
description: 'Validates the compiler configuration options',
742
- recommended: true,
756
+ preset: LintRulePreset.Recommended,
757
};
758
}
759
case ErrorCategory.EffectDependencies: {
@@ -748,7 +762,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
762
severity: ErrorSeverity.Error,
763
name: 'memoized-effect-dependencies',
764
description: 'Validates that effect dependencies are memoized',
751
- recommended: false,
765
+ preset: LintRulePreset.Off,
766
};
767
}
768
case ErrorCategory.EffectDerivationsOfState: {
@@ -758,7 +772,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
772
name: 'no-deriving-state-in-effects',
773
description:
774
'Validates against deriving values from state in an effect',
761
- recommended: false,
775
+ preset: LintRulePreset.Off,
776
};
777
}
778
case ErrorCategory.EffectSetState: {
@@ -768,7 +782,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
782
name: 'set-state-in-effect',
783
description:
784
'Validates against calling setState synchronously in an effect, which can lead to re-renders that degrade performance',
771
- recommended: true,
785
+ preset: LintRulePreset.Recommended,
786
};
787
}
788
case ErrorCategory.ErrorBoundaries: {
@@ -778,7 +792,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
792
name: 'error-boundaries',
793
description:
794
'Validates usage of error boundaries instead of try/catch for errors in child components',
781
- recommended: true,
795
+ preset: LintRulePreset.Recommended,
796
};
797
}
798
case ErrorCategory.Factories: {
@@ -789,7 +803,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
803
description:
804
'Validates against higher order functions defining nested components or hooks. ' +
805
'Components and hooks should be defined at the module level',
792
- recommended: true,
806
+ preset: LintRulePreset.Recommended,
807
};
808
}
809
case ErrorCategory.FBT: {
@@ -798,7 +812,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
812
severity: ErrorSeverity.Error,
813
name: 'fbt',
814
description: 'Validates usage of fbt',
801
- recommended: false,
815
+ preset: LintRulePreset.Off,
816
};
817
}
818
case ErrorCategory.Fire: {
@@ -807,7 +821,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
821
severity: ErrorSeverity.Error,
822
name: 'fire',
823
description: 'Validates usage of `fire`',
810
- recommended: false,
824
+ preset: LintRulePreset.Off,
825
};
826
}
827
case ErrorCategory.Gating: {
@@ -817,7 +831,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
831
name: 'gating',
832
description:
833
'Validates configuration of [gating mode](https://react.dev/reference/react-compiler/gating)',
820
- recommended: true,
834
+ preset: LintRulePreset.Recommended,
835
};
836
}
837
case ErrorCategory.Globals: {
@@ -828,7 +842,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
842
description:
843
'Validates against assignment/mutation of globals during render, part of ensuring that ' +
844
'[side effects must render outside of render](https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render)',
831
- recommended: true,
845
+ preset: LintRulePreset.Recommended,
846
};
847
}
848
case ErrorCategory.Hooks: {
@@ -842,7 +856,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
856
* We need to dedeupe these (moving the remaining bits into the compiler) and then enable
857
* this rule.
858
*/
845
- recommended: false,
859
+ preset: LintRulePreset.Off,
860
};
861
}
862
case ErrorCategory.Immutability: {
@@ -852,7 +866,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
866
name: 'immutability',
867
description:
868
'Validates against mutating props, state, and other values that [are immutable](https://react.dev/reference/rules/components-and-hooks-must-be-pure#props-and-state-are-immutable)',
855
- recommended: true,
869
+ preset: LintRulePreset.Recommended,
870
};
871
}
872
case ErrorCategory.Invariant: {
@@ -861,7 +875,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
875
severity: ErrorSeverity.Error,
876
name: 'invariant',
877
description: 'Internal invariants',
864
- recommended: false,
878
+ preset: LintRulePreset.Off,
879
};
880
}
881
case ErrorCategory.PreserveManualMemo: {
@@ -873,7 +887,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
887
'Validates that existing manual memoized is preserved by the compiler. ' +
888
'React Compiler will only compile components and hooks if its inference ' +
889
'[matches or exceeds the existing manual memoization](https://react.dev/learn/react-compiler/introduction#what-should-i-do-about-usememo-usecallback-and-reactmemo)',
876
- recommended: true,
890
+ preset: LintRulePreset.Recommended,
891
};
892
}
893
case ErrorCategory.Purity: {
@@ -883,7 +897,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
897
name: 'purity',
898
description:
899
'Validates that [components/hooks are pure](https://react.dev/reference/rules/components-and-hooks-must-be-pure) by checking that they do not call known-impure functions',
886
- recommended: true,
900
+ preset: LintRulePreset.Recommended,
901
};
902
}
903
case ErrorCategory.Refs: {
@@ -893,7 +907,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
907
name: 'refs',
908
description:
909
'Validates correct usage of refs, not reading/writing during render. See the "pitfalls" section in [`useRef()` usage](https://react.dev/reference/react/useRef#usage)',
896
- recommended: true,
910
+ preset: LintRulePreset.Recommended,
911
};
912
}
913
case ErrorCategory.RenderSetState: {
@@ -903,7 +917,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
917
name: 'set-state-in-render',
918
description:
919
'Validates against setting state during render, which can trigger additional renders and potential infinite render loops',
906
- recommended: true,
920
+ preset: LintRulePreset.Recommended,
921
};
922
}
923
case ErrorCategory.StaticComponents: {
@@ -913,7 +927,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
927
name: 'static-components',
928
description:
929
'Validates that components are static, not recreated every render. Components that are recreated dynamically can reset state and trigger excessive re-rendering',
916
- recommended: true,
930
+ preset: LintRulePreset.Recommended,
931
};
932
}
933
case ErrorCategory.Suppression: {
@@ -922,7 +936,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
936
severity: ErrorSeverity.Error,
937
name: 'rule-suppression',
938
description: 'Validates against suppression of other rules',
925
- recommended: false,
939
+ preset: LintRulePreset.Off,
940
};
941
}
942
case ErrorCategory.Syntax: {
@@ -931,7 +945,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
945
severity: ErrorSeverity.Error,
946
name: 'syntax',
947
description: 'Validates against invalid syntax',
934
- recommended: false,
948
+ preset: LintRulePreset.Off,
949
};
950
}
951
case ErrorCategory.Todo: {
@@ -940,7 +954,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
954
severity: ErrorSeverity.Hint,
955
name: 'todo',
956
description: 'Unimplemented features',
943
- recommended: false,
957
+ preset: LintRulePreset.Off,
958
};
959
}
960
case ErrorCategory.UnsupportedSyntax: {
@@ -950,7 +964,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
964
name: 'unsupported-syntax',
965
description:
966
'Validates against syntax that we do not plan to support in React Compiler',
953
- recommended: true,
967
+ preset: LintRulePreset.Recommended,
968
};
969
}
970
case ErrorCategory.UseMemo: {
@@ -960,7 +974,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
974
name: 'use-memo',
975
description:
976
'Validates usage of the useMemo() hook against common mistakes. See [`useMemo()` docs](https://react.dev/reference/react/useMemo) for more information.',
963
- recommended: true,
977
+ preset: LintRulePreset.Recommended,
978
};
979
}
980
case ErrorCategory.IncompatibleLibrary: {
@@ -970,7 +984,7 @@ function getRuleForCategoryImpl(category: ErrorCategory): LintRule {
984
name: 'incompatible-library',
985
description:
986
'Validates against usage of libraries which are incompatible with memoization (manual or automatic)',
973
- recommended: true,
987
+ preset: LintRulePreset.Recommended,
988
};
989
}
990
default: {
compiler/packages/babel-plugin-react-compiler/src/index.ts
+1
@@ -14,6 +14,7 @@ export {
14
ErrorSeverity,
15
ErrorCategory,
16
LintRules,
17
+ LintRulePreset,
18
type CompilerErrorDetailOptions,
19
type CompilerDiagnosticOptions,
20
type CompilerDiagnosticDetail,
compiler/packages/eslint-plugin-react-compiler/src/rules/ReactCompilerRule.ts
+12
-2
@@ -15,6 +15,7 @@ import type {Linter, Rule} from 'eslint';
15
import runReactCompiler, {RunCacheEntry} from '../shared/RunReactCompiler';
16
import {
17
ErrorSeverity,
18
+ LintRulePreset,
19
LintRules,
20
type LintRule,
21
} from 'babel-plugin-react-compiler/src/CompilerError';
@@ -150,7 +151,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
151
type: 'problem',
152
docs: {
153
description: rule.description,
153
- recommended: rule.recommended,
154
+ recommended: rule.preset === LintRulePreset.Recommended,
155
},
156
fixable: 'code',
157
hasSuggestions: true,
@@ -171,7 +172,16 @@ export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
172
}, {} as RulesConfig);
173
174
export const recommendedRules: RulesConfig = LintRules.filter(
174
- rule => rule.recommended,
175
+ rule => rule.preset === LintRulePreset.Recommended,
176
+).reduce((acc, rule) => {
177
+ acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
178
+ return acc;
179
+}, {} as RulesConfig);
180
+
181
+export const recommendedLatestRules: RulesConfig = LintRules.filter(
182
+ rule =>
183
+ rule.preset === LintRulePreset.Recommended ||
184
+ rule.preset === LintRulePreset.RecommendedLatest,
185
).reduce((acc, rule) => {
186
acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
187
return acc;
packages/eslint-plugin-react-hooks/src/index.ts
+19
-5
@@ -11,6 +11,7 @@ import {
11
allRules,
12
mapErrorSeverityToESlint,
13
recommendedRules,
14
+ recommendedLatestRules,
15
} from './shared/ReactCompiler';
16
import RulesOfHooks from './rules/RulesOfHooks';
17
@@ -27,7 +28,7 @@ const basicRuleConfigs = {
28
'react-hooks/exhaustive-deps': 'warn',
29
} as const satisfies Linter.RulesRecord;
30
30
-const compilerRuleConfigs = Object.fromEntries(
31
+const recommendedCompilerRuleConfigs = Object.fromEntries(
32
Object.entries(recommendedRules).map(([name, ruleConfig]) => {
33
return [
34
`react-hooks/${name}` as const,
@@ -36,9 +37,22 @@ const compilerRuleConfigs = Object.fromEntries(
37
}),
38
) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
39
39
-const allRuleConfigs: Linter.RulesRecord = {
40
+const recommendedLatestCompilerRuleConfigs = Object.fromEntries(
41
+ Object.entries(recommendedLatestRules).map(([name, ruleConfig]) => {
42
+ return [
43
+ `react-hooks/${name}` as const,
44
+ mapErrorSeverityToESlint(ruleConfig.severity),
45
+ ] as const;
46
+ }),
47
+) as Record<`react-hooks/${string}`, Linter.RuleEntry>;
48
+
49
+const recommendedRuleConfigs: Linter.RulesRecord = {
50
+ ...basicRuleConfigs,
51
+ ...recommendedCompilerRuleConfigs,
52
+};
53
+const recommendedLatestRuleConfigs: Linter.RulesRecord = {
54
...basicRuleConfigs,
41
- ...compilerRuleConfigs,
55
+ ...recommendedLatestCompilerRuleConfigs,
56
};
57
58
const plugins = ['react-hooks'];
@@ -51,11 +65,11 @@ type ReactHooksFlatConfig = {
65
const configs = {
66
recommended: {
67
plugins,
54
- rules: allRuleConfigs,
68
+ rules: recommendedRuleConfigs,
69
},
70
'recommended-latest': {
71
plugins,
58
- rules: allRuleConfigs,
72
+ rules: recommendedLatestRuleConfigs,
73
},
74
flat: {} as Record<string, ReactHooksFlatConfig>,
75
};
packages/eslint-plugin-react-hooks/src/shared/ReactCompiler.ts
+20
-16
@@ -14,6 +14,7 @@ import {
14
LintRules,
15
type LintRule,
16
ErrorSeverity,
17
+ LintRulePreset,
18
} from 'babel-plugin-react-compiler';
19
import {type Linter, type Rule} from 'eslint';
20
import runReactCompiler, {RunCacheEntry} from './RunReactCompiler';
@@ -149,7 +150,7 @@ function makeRule(rule: LintRule): Rule.RuleModule {
150
type: 'problem',
151
docs: {
152
description: rule.description,
152
- recommended: rule.recommended,
153
+ recommended: rule.preset === LintRulePreset.Recommended,
154
},
155
fixable: 'code',
156
hasSuggestions: true,
@@ -164,23 +165,26 @@ type RulesConfig = {
165
[name: string]: {rule: Rule.RuleModule; severity: ErrorSeverity};
166
};
167
167
-export const allRules: RulesConfig = LintRules.reduce(
168
- (acc, rule) => {
169
- acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
170
- return acc;
171
- },
172
- {} as RulesConfig,
173
-);
168
+export const allRules: RulesConfig = LintRules.reduce((acc, rule) => {
169
+ acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
170
+ return acc;
171
+}, {} as RulesConfig);
172
173
export const recommendedRules: RulesConfig = LintRules.filter(
176
- rule => rule.recommended,
177
-).reduce(
178
- (acc, rule) => {
179
- acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
180
- return acc;
181
- },
182
- {} as RulesConfig,
183
-);
174
+ rule => rule.preset === LintRulePreset.Recommended,
175
+).reduce((acc, rule) => {
176
+ acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
177
+ return acc;
178
+}, {} as RulesConfig);
179
+
180
+export const recommendedLatestRules: RulesConfig = LintRules.filter(
181
+ rule =>
182
+ rule.preset === LintRulePreset.Recommended ||
183
+ rule.preset === LintRulePreset.RecommendedLatest,
184
+).reduce((acc, rule) => {
185
+ acc[rule.name] = {rule: makeRule(rule), severity: rule.severity};
186
+ return acc;
187
+}, {} as RulesConfig);
188
189
export function mapErrorSeverityToESlint(
190
severity: ErrorSeverity,