Warn when second callback is passed to setState/dispatch in Hooks (#14625)
Dan Abramov committed
Jan 18, 2019 at 18:48 UTC
177fb7635391fb9c56ba3b8e38fd45131b73ebe7
2 files changed
+73
packages/react-reconciler/src/ReactFiberHooks.js
+9
@@ -782,6 +782,15 @@ function dispatchAction<S, A>(
782
'an infinite loop.',
783
);
784
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
+
794
const alternate = fiber.alternate;
795
if (
796
fiber === currentlyRenderingFiber ||
packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js
+64
@@ -183,6 +183,70 @@ describe('ReactHooks', () => {
183
expect(root).toFlushAndYield(['Parent: 1, 2 (dark)']);
184
});
185
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
+
250
it('never bails out if context has changed', () => {
251
const {useState, useLayoutEffect, useContext} = React;
252