Skip to content

Commit 177fb76

Browse files
authored
Warn when second callback is passed to setState/dispatch in Hooks (#14625)
1 parent d17d0b9 commit 177fb76

File tree

2 files changed

+73
-0
lines changed

2 files changed

+73
-0
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

+9
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,15 @@ function dispatchAction<S, A>(
782782
'an infinite loop.',
783783
);
784784

785+
if (__DEV__) {
786+
warning(
787+
arguments.length <= 3,
788+
"State updates from the useState() and useReducer() Hooks don't support the " +
789+
'second callback argument. To execute a side effect after ' +
790+
'rendering, declare it in the component body with useEffect().',
791+
);
792+
}
793+
785794
const alternate = fiber.alternate;
786795
if (
787796
fiber === currentlyRenderingFiber ||

packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js

+64
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,70 @@ describe('ReactHooks', () => {
183183
expect(root).toFlushAndYield(['Parent: 1, 2 (dark)']);
184184
});
185185

186+
it('warns about setState second argument', () => {
187+
const {useState} = React;
188+
189+
let setCounter;
190+
function Counter() {
191+
const [counter, _setCounter] = useState(0);
192+
setCounter = _setCounter;
193+
194+
ReactTestRenderer.unstable_yield(`Count: ${counter}`);
195+
return counter;
196+
}
197+
198+
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
199+
root.update(<Counter />);
200+
expect(root).toFlushAndYield(['Count: 0']);
201+
expect(root).toMatchRenderedOutput('0');
202+
203+
expect(() => {
204+
setCounter(1, () => {
205+
throw new Error('Expected to ignore the callback.');
206+
});
207+
}).toWarnDev(
208+
'State updates from the useState() and useReducer() Hooks ' +
209+
"don't support the second callback argument. " +
210+
'To execute a side effect after rendering, ' +
211+
'declare it in the component body with useEffect().',
212+
{withoutStack: true},
213+
);
214+
expect(root).toFlushAndYield(['Count: 1']);
215+
expect(root).toMatchRenderedOutput('1');
216+
});
217+
218+
it('warns about dispatch second argument', () => {
219+
const {useReducer} = React;
220+
221+
let dispatch;
222+
function Counter() {
223+
const [counter, _dispatch] = useReducer((s, a) => a, 0);
224+
dispatch = _dispatch;
225+
226+
ReactTestRenderer.unstable_yield(`Count: ${counter}`);
227+
return counter;
228+
}
229+
230+
const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
231+
root.update(<Counter />);
232+
expect(root).toFlushAndYield(['Count: 0']);
233+
expect(root).toMatchRenderedOutput('0');
234+
235+
expect(() => {
236+
dispatch(1, () => {
237+
throw new Error('Expected to ignore the callback.');
238+
});
239+
}).toWarnDev(
240+
'State updates from the useState() and useReducer() Hooks ' +
241+
"don't support the second callback argument. " +
242+
'To execute a side effect after rendering, ' +
243+
'declare it in the component body with useEffect().',
244+
{withoutStack: true},
245+
);
246+
expect(root).toFlushAndYield(['Count: 1']);
247+
expect(root).toMatchRenderedOutput('1');
248+
});
249+
186250
it('never bails out if context has changed', () => {
187251
const {useState, useLayoutEffect, useContext} = React;
188252

0 commit comments

Comments
 (0)