From f11a9c1cb06514e40a50503e0f9bd26941251e06 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 24 Jan 2019 16:10:13 -0800 Subject: [PATCH] State update bug in concurrent mode (#14698) * State update bug in concurrent mode * Fix bug introduced by double-rendering Functions using hooks --- .../src/__tests__/ReactDOMHooks-test.js | 103 ++++++++++++++++++ .../src/ReactFiberBeginWork.js | 6 +- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMHooks-test.js diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js new file mode 100644 index 0000000000000..d317e407e2e1b --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; + +describe('ReactDOMSuspensePlaceholder', () => { + let container; + + beforeEach(() => { + jest.resetModules(); + + React = require('react'); + ReactDOM = require('react-dom'); + + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + it('should not bail out when an update is scheduled from within an event handler', () => { + const {createRef, useCallback, useState} = React; + + const Example = ({inputRef, labelRef}) => { + const [text, setText] = useState(''); + const handleInput = useCallback(event => { + setText(event.target.value); + }); + + return ( + + + + + ); + }; + + const inputRef = createRef(); + const labelRef = createRef(); + + ReactDOM.render( + , + container, + ); + + inputRef.current.value = 'abc'; + inputRef.current.dispatchEvent( + new Event('input', {bubbles: true, cancelable: true}), + ); + + expect(labelRef.current.innerHTML).toBe('abc'); + }); + + it('should not bail out when an update is scheduled from within an event handler in ConcurrentMode', () => { + const {createRef, useCallback, useState} = React; + + const Example = ({inputRef, labelRef}) => { + const [text, setText] = useState(''); + const handleInput = useCallback(event => { + setText(event.target.value); + }); + + return ( + + + + + ); + }; + + const inputRef = createRef(); + const labelRef = createRef(); + + const root = ReactDOM.unstable_createRoot(container); + root.render( + + + , + ); + + jest.runAllTimers(); + + inputRef.current.value = 'abc'; + inputRef.current.dispatchEvent( + new Event('input', {bubbles: true, cancelable: true}), + ); + + jest.runAllTimers(); + + expect(labelRef.current.innerHTML).toBe('abc'); + }); +}); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 9c4a72b7fedcb..e4420d7004e8e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -257,7 +257,7 @@ function updateForwardRef( ) { // Only double-render components with Hooks if (workInProgress.memoizedState !== null) { - renderWithHooks( + nextChildren = renderWithHooks( current, workInProgress, render, @@ -567,7 +567,7 @@ function updateFunctionComponent( ) { // Only double-render components with Hooks if (workInProgress.memoizedState !== null) { - renderWithHooks( + nextChildren = renderWithHooks( current, workInProgress, Component, @@ -1252,7 +1252,7 @@ function mountIndeterminateComponent( ) { // Only double-render components with Hooks if (workInProgress.memoizedState !== null) { - renderWithHooks( + value = renderWithHooks( null, workInProgress, Component,