diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 29e956d314a2..3f2852a13d44 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -612,6 +612,29 @@ const tests = { } `, }, + { + // The dispatch function returned by useActionState is stable, including + // when destructuring the third `isPending` element: + // const [state, dispatch, isPending] = useActionState(...) + code: normalizeIndent` + function MyComponent() { + const [state, dispatch, isPending] = useActionState(action, 0); + useEffect(() => { + dispatch(); + }, []); + } + `, + }, + { + code: normalizeIndent` + function MyComponent() { + const [state, dispatch, isPending] = React.useActionState(action, 0); + useEffect(() => { + dispatch(); + }, []); + } + `, + }, { code: normalizeIndent` function MyComponent({ maybeRef2, foo }) { @@ -1626,6 +1649,41 @@ const tests = { }, ], }, + { + // The state and isPending values from useActionState are dynamic, so + // they still need to be listed as dependencies (only the dispatch + // function is stable). + code: normalizeIndent` + function MyComponent() { + const [state, dispatch, isPending] = useActionState(action, 0); + useEffect(() => { + console.log(state, isPending); + dispatch(); + }, []); + } + `, + errors: [ + { + message: + "React Hook useEffect has missing dependencies: 'isPending' and 'state'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [isPending, state]', + output: normalizeIndent` + function MyComponent() { + const [state, dispatch, isPending] = useActionState(action, 0); + useEffect(() => { + console.log(state, isPending); + dispatch(); + }, [isPending, state]); + } + `, + }, + ], + }, + ], + }, { // Affected code should use React.useActionState instead code: normalizeIndent` diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index 6b790680608d..343539c43c23 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -351,9 +351,13 @@ const rule = { name === 'useActionState' ) { // Only consider second value in initializing tuple stable. + // useState and useReducer return a 2-element tuple, while + // useActionState additionally returns `isPending` as a third element: + // const [state, dispatch, isPending] = useActionState(...) if ( id.type === 'ArrayPattern' && - id.elements.length === 2 && + (id.elements.length === 2 || + (name === 'useActionState' && id.elements.length === 3)) && isArray(resolved.identifiers) ) { // Is second tuple value the same reference we're checking?