From 177fb7635391fb9c56ba3b8e38fd45131b73ebe7 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 18 Jan 2019 18:48:58 +0000 Subject: [PATCH] Warn when second callback is passed to setState/dispatch in Hooks (#14625) --- .../react-reconciler/src/ReactFiberHooks.js | 9 +++ .../src/__tests__/ReactHooks-test.internal.js | 64 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 567d286029b2d..4af03961690c5 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -782,6 +782,15 @@ function dispatchAction( 'an infinite loop.', ); + if (__DEV__) { + warning( + arguments.length <= 3, + "State updates from the useState() and useReducer() Hooks don't support the " + + 'second callback argument. To execute a side effect after ' + + 'rendering, declare it in the component body with useEffect().', + ); + } + const alternate = fiber.alternate; if ( fiber === currentlyRenderingFiber || diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 37a646d73757a..19fa96e8cdc19 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -183,6 +183,70 @@ describe('ReactHooks', () => { expect(root).toFlushAndYield(['Parent: 1, 2 (dark)']); }); + it('warns about setState second argument', () => { + const {useState} = React; + + let setCounter; + function Counter() { + const [counter, _setCounter] = useState(0); + setCounter = _setCounter; + + ReactTestRenderer.unstable_yield(`Count: ${counter}`); + return counter; + } + + const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); + root.update(); + expect(root).toFlushAndYield(['Count: 0']); + expect(root).toMatchRenderedOutput('0'); + + expect(() => { + setCounter(1, () => { + throw new Error('Expected to ignore the callback.'); + }); + }).toWarnDev( + 'State updates from the useState() and useReducer() Hooks ' + + "don't support the second callback argument. " + + 'To execute a side effect after rendering, ' + + 'declare it in the component body with useEffect().', + {withoutStack: true}, + ); + expect(root).toFlushAndYield(['Count: 1']); + expect(root).toMatchRenderedOutput('1'); + }); + + it('warns about dispatch second argument', () => { + const {useReducer} = React; + + let dispatch; + function Counter() { + const [counter, _dispatch] = useReducer((s, a) => a, 0); + dispatch = _dispatch; + + ReactTestRenderer.unstable_yield(`Count: ${counter}`); + return counter; + } + + const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true}); + root.update(); + expect(root).toFlushAndYield(['Count: 0']); + expect(root).toMatchRenderedOutput('0'); + + expect(() => { + dispatch(1, () => { + throw new Error('Expected to ignore the callback.'); + }); + }).toWarnDev( + 'State updates from the useState() and useReducer() Hooks ' + + "don't support the second callback argument. " + + 'To execute a side effect after rendering, ' + + 'declare it in the component body with useEffect().', + {withoutStack: true}, + ); + expect(root).toFlushAndYield(['Count: 1']); + expect(root).toMatchRenderedOutput('1'); + }); + it('never bails out if context has changed', () => { const {useState, useLayoutEffect, useContext} = React;