react/fixtures/eslint-v6/index.js
lauren 26b177bc5e
[eprh] Fix recommended config for flat config compatibility (#34700)
Previously, the `recommended` config used the legacy ESLint format
(plugins as an array of strings). This causes errors when used with
ESLint v9's `defineConfig()` helper. This was following [eslint's own
docs](https://eslint.org/docs/latest/extend/plugins#backwards-compatibility-for-legacy-configs):

> With this approach, both configuration systems recognize
"recommended". The old config system uses the recommended key while the
current config system uses the flat/recommended key. The defineConfig()
helper first looks at the recommended key, and if that is not in the
correct format, it looks for the flat/recommended key. This allows you
an upgrade path if you’d later like to rename flat/recommended to
recommended when you no longer need to support the old config system.

However,
[`isLegacyConfig()`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L73-L81)
(also see
[`eslintrcKeys`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L24-L35))
function doesn't check for the `plugins` key, so our config was
incorrectly treated as flat config despite being in legacy format.

This PR fixes the issue, along with a few other fixes combined:

1. Convert `recommended` to flat config format
2. Separate basic rules (exhaustive-deps, rules-of-hooks) from compiler
rules
3. Add `recommended-latest-legacy` config for non-flat config users who
want all recommended rules (including compiler rules)
4. Adding more types for the exported config

Our shipped presets in 6.x.x will essentially be:
- `recommended-legacy`: legacy (non-flat), with basic rules only
- `recommended-latest-legacy`: legacy (non-flat), all rules (basic +
compiler)
- `flat/recommended`: flat, basic rules only (now the same as
recommended, but to avoid making a breaking change we'll just keep it
around in 6.x.x)
- `recommended-latest`: flat, all rules (basic + compiler)
- `recommended`: flat, basic rules only

In the next breaking release 7.x.x, we will collapse down the presets
into three:

- `recommended-legacy`: all recommended rules
- `recommended`: all recommended rules
- `recommended-experimental`: all recommended rules + new bleeding edge
experimental rules

Closes #34679

---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34700).
* #34703
* __->__ #34700
2025-10-02 18:52:52 -04:00

168 lines
4.2 KiB
JavaScript

/**
* Exhaustive Deps
*/
// Valid because dependencies are declared correctly
function Comment({comment, commentSource}) {
const currentUserID = comment.viewer.id;
const environment = RelayEnvironment.forUser(currentUserID);
const commentID = nullthrows(comment.id);
useEffect(() => {
const subscription = SubscriptionCounter.subscribeOnce(
`StoreSubscription_${commentID}`,
() =>
StoreSubscription.subscribe(
environment,
{
comment_id: commentID,
},
currentUserID,
commentSource
)
);
return () => subscription.dispose();
}, [commentID, commentSource, currentUserID, environment]);
}
// Valid because no dependencies
function UseEffectWithNoDependencies() {
const local = {};
useEffect(() => {
console.log(local);
});
}
function UseEffectWithEmptyDependencies() {
useEffect(() => {
const local = {};
console.log(local);
}, []);
}
// OK because `props` wasn't defined.
function ComponentWithNoPropsDefined() {
useEffect(() => {
console.log(props.foo);
}, []);
}
// Valid because props are declared as a dependency
function ComponentWithPropsDeclaredAsDep({foo}) {
useEffect(() => {
console.log(foo.length);
console.log(foo.slice(0));
}, [foo]);
}
// Valid because individual props are declared as dependencies
function ComponentWithIndividualPropsDeclaredAsDeps(props) {
useEffect(() => {
console.log(props.foo);
console.log(props.bar);
}, [props.bar, props.foo]);
}
// Invalid because neither props or props.foo are declared as dependencies
function ComponentWithoutDeclaringPropAsDep(props) {
useEffect(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useCallback(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useMemo(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useCallback(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useMemo(() => {
console.log(props.foo);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.notReactiveHook(() => {
console.log(props.foo);
}, []); // This one isn't a violation
}
/**
* Rules of Hooks
*/
// Valid because functions can call functions.
function normalFunctionWithConditionalFunction() {
if (cond) {
doSomething();
}
}
// Valid because hooks can call hooks.
function useHook() {
useState();
}
const whatever = function useHook() {
useState();
};
const useHook1 = () => {
useState();
};
let useHook2 = () => useState();
useHook2 = () => {
useState();
};
// Invalid because hooks can't be called in conditionals.
function ComponentWithConditionalHook() {
if (cond) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useConditionalHook();
}
}
// Invalid because hooks can't be called in loops.
function useHookInLoops() {
while (a) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook1();
if (b) return;
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook2();
}
while (c) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook3();
if (d) return;
// eslint-disable-next-line react-hooks/rules-of-hooks
useHook4();
}
}
/**
* Compiler Rules
*/
// Invalid: component factory
function InvalidComponentFactory() {
const DynamicComponent = () => <div>Hello</div>;
// eslint-disable-next-line react-hooks/static-components
return <DynamicComponent />;
}
// Invalid: mutating globals
function InvalidGlobals() {
// eslint-disable-next-line react-hooks/immutability
window.myGlobal = 42;
return <div>Done</div>;
}
// Invalid: useMemo with wrong deps - triggers preserve-manual-memoization
function InvalidUseMemo({items}) {
// eslint-disable-next-line react-hooks/preserve-manual-memoization, react-hooks/exhaustive-deps
const sorted = useMemo(() => [...items].sort(), []);
return <div>{sorted.length}</div>;
}