State update bug in concurrent mode (#14698)
* State update bug in concurrent mode * Fix bug introduced by double-rendering Functions using hooks
Brian Vaughn committed
Jan 24, 2019 at 16:10 UTC
f11a9c1cb06514e40a50503e0f9bd26941251e06
2 files changed
+106
-3
packages/react-dom/src/__tests__/ReactDOMHooks-test.js
new
+103
@@ -0,0 +1,103 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React;
13
+let ReactDOM;
14
+
15
+describe('ReactDOMSuspensePlaceholder', () => {
16
+ let container;
17
+
18
+ beforeEach(() => {
19
+ jest.resetModules();
20
+
21
+ React = require('react');
22
+ ReactDOM = require('react-dom');
23
+
24
+ container = document.createElement('div');
25
+ document.body.appendChild(container);
26
+ });
27
+
28
+ afterEach(() => {
29
+ document.body.removeChild(container);
30
+ });
31
+
32
+ it('should not bail out when an update is scheduled from within an event handler', () => {
33
+ const {createRef, useCallback, useState} = React;
34
+
35
+ const Example = ({inputRef, labelRef}) => {
36
+ const [text, setText] = useState('');
37
+ const handleInput = useCallback(event => {
38
+ setText(event.target.value);
39
+ });
40
+
41
+ return (
42
+ <React.Fragment>
43
+ <input ref={inputRef} onInput={handleInput} />
44
+ <label ref={labelRef}>{text}</label>
45
+ </React.Fragment>
46
+ );
47
+ };
48
+
49
+ const inputRef = createRef();
50
+ const labelRef = createRef();
51
+
52
+ ReactDOM.render(
53
+ <Example inputRef={inputRef} labelRef={labelRef} />,
54
+ container,
55
+ );
56
+
57
+ inputRef.current.value = 'abc';
58
+ inputRef.current.dispatchEvent(
59
+ new Event('input', {bubbles: true, cancelable: true}),
60
+ );
61
+
62
+ expect(labelRef.current.innerHTML).toBe('abc');
63
+ });
64
+
65
+ it('should not bail out when an update is scheduled from within an event handler in ConcurrentMode', () => {
66
+ const {createRef, useCallback, useState} = React;
67
+
68
+ const Example = ({inputRef, labelRef}) => {
69
+ const [text, setText] = useState('');
70
+ const handleInput = useCallback(event => {
71
+ setText(event.target.value);
72
+ });
73
+
74
+ return (
75
+ <React.Fragment>
76
+ <input ref={inputRef} onInput={handleInput} />
77
+ <label ref={labelRef}>{text}</label>
78
+ </React.Fragment>
79
+ );
80
+ };
81
+
82
+ const inputRef = createRef();
83
+ const labelRef = createRef();
84
+
85
+ const root = ReactDOM.unstable_createRoot(container);
86
+ root.render(
87
+ <React.unstable_ConcurrentMode>
88
+ <Example inputRef={inputRef} labelRef={labelRef} />
89
+ </React.unstable_ConcurrentMode>,
90
+ );
91
+
92
+ jest.runAllTimers();
93
+
94
+ inputRef.current.value = 'abc';
95
+ inputRef.current.dispatchEvent(
96
+ new Event('input', {bubbles: true, cancelable: true}),
97
+ );
98
+
99
+ jest.runAllTimers();
100
+
101
+ expect(labelRef.current.innerHTML).toBe('abc');
102
+ });
103
+});
packages/react-reconciler/src/ReactFiberBeginWork.js
+3
-3
@@ -257,7 +257,7 @@ function updateForwardRef(
257
) {
258
// Only double-render components with Hooks
259
if (workInProgress.memoizedState !== null) {
260
- renderWithHooks(
260
+ nextChildren = renderWithHooks(
261
current,
262
workInProgress,
263
render,
@@ -567,7 +567,7 @@ function updateFunctionComponent(
567
) {
568
// Only double-render components with Hooks
569
if (workInProgress.memoizedState !== null) {
570
- renderWithHooks(
570
+ nextChildren = renderWithHooks(
571
current,
572
workInProgress,
573
Component,
@@ -1252,7 +1252,7 @@ function mountIndeterminateComponent(
1252
) {
1253
// Only double-render components with Hooks
1254
if (workInProgress.memoizedState !== null) {
1255
- renderWithHooks(
1255
+ value = renderWithHooks(
1256
null,
1257
workInProgress,
1258
Component,