From b024f7e4ac5ca9e51a27c932c57471d13634228a Mon Sep 17 00:00:00 2001 From: eps1lon Date: Thu, 30 May 2024 15:00:17 +0200 Subject: [PATCH 1/3] Failing test for dispatch from useActionState --- .../__tests__/ESLintRuleExhaustiveDeps-test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 235d60349b6ec..a3834a2503b8a 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -607,6 +607,10 @@ const tests = { const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + const [state9, dispatch5] = useActionState(); + const [state10, dispatch6] = React.useActionState(); const [isPending1] = useTransition(); const [isPending2, startTransition2] = useTransition(); const [isPending3] = React.useTransition(); @@ -624,6 +628,10 @@ const tests = { setState2(); dispatch1(); dispatch2(); + dispatch3(); + dispatch4(); + dispatch5(); + dispatch6(); startTransition1(); startTransition2(); startTransition3(); @@ -646,7 +654,7 @@ const tests = { maybeDispatch(); }, [ // Dynamic - state1, state2, state3, state4, state5, state6, + state1, state2, state3, state4, state5, state6, state7, state8, state9, state10, maybeRef1, maybeRef2, isPending2, isPending4, From 1e97088bd72c62ab8b96053e1b0abc1e2500b543 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Thu, 30 May 2024 15:00:29 +0200 Subject: [PATCH 2/3] Consider dispatch from `useActionState` and `useFormState` stable --- .../src/ExhaustiveDeps.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index f012428961900..247e5febf7217 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -179,6 +179,10 @@ export default { // ^^^ true for this reference // const [state, dispatch] = useReducer() / React.useReducer() // ^^^ true for this reference + // const [state, dispatch] = useActionState() / React.useActionState() + // ^^^ true for this reference + // const [state, dispatch] = useFormState() / ReactDOM.useFormState() + // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // const onStuff = useEffectEvent(() => {}) @@ -232,10 +236,11 @@ export default { return false; } let callee = init.callee; - // Step into `= React.something` initializer. + // Step into `= React(DOM).something` initializer. if ( callee.type === 'MemberExpression' && - callee.object.name === 'React' && + (callee.object.name === 'React' || + callee.object.name === 'ReactDOM') && callee.property != null && !callee.computed ) { @@ -260,7 +265,12 @@ export default { } // useEffectEvent() return value is always unstable. return true; - } else if (name === 'useState' || name === 'useReducer') { + } else if ( + name === 'useState' || + name === 'useReducer' || + name === 'useFormState' || + name === 'useActionState' + ) { // Only consider second value in initializing tuple stable. if ( id.type === 'ArrayPattern' && From 510f0534630f51fffcccebe41803da0bad6a4eae Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 24 Jun 2024 09:33:13 +0200 Subject: [PATCH 3/3] Ignore ReactDOM.useFormState --- .../ESLintRuleExhaustiveDeps-test.js | 51 +++++++++++++++++-- .../src/ExhaustiveDeps.js | 8 +-- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index a3834a2503b8a..c9ba00f2139f2 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -607,8 +607,6 @@ const tests = { const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); - const [state7, dispatch3] = useFormState(); - const [state8, dispatch4] = ReactDOM.useFormState(); const [state9, dispatch5] = useActionState(); const [state10, dispatch6] = React.useActionState(); const [isPending1] = useTransition(); @@ -628,8 +626,6 @@ const tests = { setState2(); dispatch1(); dispatch2(); - dispatch3(); - dispatch4(); dispatch5(); dispatch6(); startTransition1(); @@ -654,7 +650,7 @@ const tests = { maybeDispatch(); }, [ // Dynamic - state1, state2, state3, state4, state5, state6, state7, state8, state9, state10, + state1, state2, state3, state4, state5, state6, state9, state10, maybeRef1, maybeRef2, isPending2, isPending4, @@ -1502,6 +1498,51 @@ const tests = { }, ], }, + { + // Affected code should use React.useActionState instead + code: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [state7, state8]); + } + `, + errors: [ + { + message: + "React Hook useEffect has missing dependencies: 'dispatch3' and 'dispatch4'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [dispatch3, dispatch4, state7, state8]', + output: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [dispatch3, dispatch4, state7, state8]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index 247e5febf7217..48ccc1e6bb818 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -181,8 +181,6 @@ export default { // ^^^ true for this reference // const [state, dispatch] = useActionState() / React.useActionState() // ^^^ true for this reference - // const [state, dispatch] = useFormState() / ReactDOM.useFormState() - // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // const onStuff = useEffectEvent(() => {}) @@ -236,11 +234,10 @@ export default { return false; } let callee = init.callee; - // Step into `= React(DOM).something` initializer. + // Step into `= React.something` initializer. if ( callee.type === 'MemberExpression' && - (callee.object.name === 'React' || - callee.object.name === 'ReactDOM') && + callee.object.name === 'React' && callee.property != null && !callee.computed ) { @@ -268,7 +265,6 @@ export default { } else if ( name === 'useState' || name === 'useReducer' || - name === 'useFormState' || name === 'useActionState' ) { // Only consider second value in initializing tuple stable.