mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
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
168 lines
4.2 KiB
JavaScript
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>;
|
|
}
|