diff --git a/fixtures/dom/.gitignore b/fixtures/dom/.gitignore index 9f05c1cc2b73..724d1459422c 100644 --- a/fixtures/dom/.gitignore +++ b/fixtures/dom/.gitignore @@ -14,6 +14,8 @@ public/react-dom.development.js public/react-dom.production.min.js public/react-dom-server.browser.development.js public/react-dom-server.browser.production.min.js +public/react-dom-test-utils.development.js +public/react-dom-test-utils.production.min.js # misc .DS_Store diff --git a/fixtures/dom/package.json b/fixtures/dom/package.json index 940b66736ce4..f5f84257f7c8 100644 --- a/fixtures/dom/package.json +++ b/fixtures/dom/package.json @@ -18,7 +18,7 @@ }, "scripts": { "start": "react-scripts start", - "prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js public/", + "prestart": "cp ../../build/node_modules/react/umd/react.development.js ../../build/node_modules/react-dom/umd/react-dom.development.js ../../build/node_modules/react/umd/react.production.min.js ../../build/node_modules/react-dom/umd/react-dom.production.min.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.development.js ../../build/node_modules/react-dom/umd/react-dom-server.browser.production.min.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.development.js ../../build/node_modules/react-dom/umd/react-dom-test-utils.production.min.js public/", "build": "react-scripts build && cp build/index.html build/200.html", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" diff --git a/fixtures/dom/public/act-dom.html b/fixtures/dom/public/act-dom.html new file mode 100644 index 000000000000..2fb4a437721d --- /dev/null +++ b/fixtures/dom/public/act-dom.html @@ -0,0 +1,41 @@ + + + + sanity test for ReactTestUtils.act + + + this page tests whether act runs properly in a browser. +
+ your console should say "5" + + + + + + diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index 3e35c28dbc20..687dbd1aaec2 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -14,7 +14,6 @@ let React; let ReactDOM; let ReactDOMServer; let ReactTestUtils; -let act; function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -34,7 +33,6 @@ describe('ReactTestUtils', () => { ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); - act = ReactTestUtils.act; }); it('Simulate should have locally attached media events', () => { @@ -517,173 +515,4 @@ describe('ReactTestUtils', () => { ReactTestUtils.renderIntoDocument(); expect(mockArgs.length).toEqual(0); }); - - it('can use act to batch effects', () => { - function App(props) { - React.useEffect(props.callback); - return null; - } - const container = document.createElement('div'); - document.body.appendChild(container); - - try { - let called = false; - act(() => { - ReactDOM.render( - { - called = true; - }} - />, - container, - ); - }); - - expect(called).toBe(true); - } finally { - document.body.removeChild(container); - } - }); - - it('flushes effects on every call', () => { - function App(props) { - let [ctr, setCtr] = React.useState(0); - React.useEffect(() => { - props.callback(ctr); - }); - return ( - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - let calledCtr = 0; - act(() => { - ReactDOM.render( - { - calledCtr = val; - }} - />, - container, - ); - }); - const button = document.getElementById('button'); - function click() { - button.dispatchEvent(new MouseEvent('click', {bubbles: true})); - } - - act(() => { - click(); - click(); - click(); - }); - expect(calledCtr).toBe(3); - act(click); - expect(calledCtr).toBe(4); - act(click); - expect(calledCtr).toBe(5); - - document.body.removeChild(container); - }); - - it('can use act to batch effects on updates too', () => { - function App() { - let [ctr, setCtr] = React.useState(0); - return ( - - ); - } - const container = document.createElement('div'); - document.body.appendChild(container); - let button; - act(() => { - ReactDOM.render(, container); - }); - button = document.getElementById('button'); - expect(button.innerHTML).toBe('0'); - act(() => { - button.dispatchEvent(new MouseEvent('click', {bubbles: true})); - }); - expect(button.innerHTML).toBe('1'); - document.body.removeChild(container); - }); - - it('detects setState being called outside of act(...)', () => { - let setValueRef = null; - function App() { - let [value, setValue] = React.useState(0); - setValueRef = setValue; - return ( - - ); - } - const container = document.createElement('div'); - document.body.appendChild(container); - let button; - act(() => { - ReactDOM.render(, container); - button = container.querySelector('#button'); - button.dispatchEvent(new MouseEvent('click', {bubbles: true})); - }); - expect(button.innerHTML).toBe('2'); - expect(() => setValueRef(1)).toWarnDev([ - 'An update to App inside a test was not wrapped in act(...).', - ]); - document.body.removeChild(container); - }); - - it('lets a ticker update', () => { - function App() { - let [toggle, setToggle] = React.useState(0); - React.useEffect(() => { - let timeout = setTimeout(() => { - setToggle(1); - }, 200); - return () => clearTimeout(timeout); - }); - return toggle; - } - const container = document.createElement('div'); - - act(() => { - act(() => { - ReactDOM.render(, container); - }); - jest.advanceTimersByTime(250); - }); - - expect(container.innerHTML).toBe('1'); - }); - - it('warns if you return a value inside act', () => { - expect(() => act(() => null)).toWarnDev( - [ - 'The callback passed to ReactTestUtils.act(...) function must not return anything.', - ], - {withoutStack: true}, - ); - expect(() => act(() => 123)).toWarnDev( - [ - 'The callback passed to ReactTestUtils.act(...) function must not return anything.', - ], - {withoutStack: true}, - ); - }); - - it('warns if you try to await an .act call', () => { - expect(act(() => {}).then).toWarnDev( - [ - 'Do not await the result of calling ReactTestUtils.act(...), it is not a Promise.', - ], - {withoutStack: true}, - ); - }); }); diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js new file mode 100644 index 000000000000..6e037088c920 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -0,0 +1,403 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * 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 + */ + +let React; +let ReactDOM; +let ReactTestUtils; +let act; + +jest.useRealTimers(); + +function sleep(period) { + return new Promise(resolve => { + setTimeout(() => { + resolve(true); + }, period); + }); +} + +describe('ReactTestUtils.act()', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactTestUtils = require('react-dom/test-utils'); + act = ReactTestUtils.act; + }); + + describe('sync', () => { + it('can use act to flush effects', () => { + function App(props) { + React.useEffect(props.callback); + return null; + } + + let calledLog = []; + act(() => { + ReactDOM.render( + { + calledLog.push(calledLog.length); + }} + />, + document.createElement('div'), + ); + }); + + expect(calledLog).toEqual([0]); + }); + + it('flushes effects on every call', () => { + function App(props) { + let [ctr, setCtr] = React.useState(0); + React.useEffect(() => { + props.callback(ctr); + }); + return ( + + ); + } + + const container = document.createElement('div'); + // attach to body so events works + document.body.appendChild(container); + let calledCounter = 0; + act(() => { + ReactDOM.render( + { + calledCounter = val; + }} + />, + container, + ); + }); + const button = document.getElementById('button'); + function click() { + button.dispatchEvent(new MouseEvent('click', {bubbles: true})); + } + + act(() => { + click(); + click(); + click(); + }); + expect(calledCounter).toBe(3); + act(click); + expect(calledCounter).toBe(4); + act(click); + expect(calledCounter).toBe(5); + expect(button.innerHTML).toBe('5'); + + document.body.removeChild(container); + }); + + it('should flush effects recursively', () => { + function App() { + let [ctr, setCtr] = React.useState(0); + React.useEffect(() => { + if (ctr < 5) { + setCtr(x => x + 1); + } + }); + return ctr; + } + + const container = document.createElement('div'); + act(() => { + ReactDOM.render(, container); + }); + + expect(container.innerHTML).toBe('5'); + }); + + it('detects setState being called outside of act(...)', () => { + let setValue = null; + function App() { + let [value, _setValue] = React.useState(0); + setValue = _setValue; + return ( + + ); + } + const container = document.createElement('div'); + document.body.appendChild(container); + let button; + act(() => { + ReactDOM.render(, container); + button = container.querySelector('#button'); + button.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(button.innerHTML).toBe('2'); + expect(() => setValue(1)).toWarnDev([ + 'An update to App inside a test was not wrapped in act(...).', + ]); + document.body.removeChild(container); + }); + describe('fake timers', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + it('lets a ticker update', () => { + function App() { + let [toggle, setToggle] = React.useState(0); + React.useEffect(() => { + let timeout = setTimeout(() => { + setToggle(1); + }, 200); + return () => clearTimeout(timeout); + }, []); + return toggle; + } + const container = document.createElement('div'); + + act(() => { + ReactDOM.render(, container); + }); + act(() => { + jest.runAllTimers(); + }); + + expect(container.innerHTML).toBe('1'); + }); + it('can use the async version to catch microtasks', async () => { + function App() { + let [toggle, setToggle] = React.useState(0); + React.useEffect(() => { + // just like the previous test, except we + // use a promise and schedule the update + // after it resolves + sleep(200).then(() => setToggle(1)); + }, []); + return toggle; + } + const container = document.createElement('div'); + + act(() => { + ReactDOM.render(, container); + }); + await act(async () => { + jest.runAllTimers(); + }); + + expect(container.innerHTML).toBe('1'); + }); + it('can handle cascading promises with fake timers', async () => { + // this component triggers an effect, that waits a tick, + // then sets state. repeats this 5 times. + function App() { + let [state, setState] = React.useState(0); + async function ticker() { + await null; + setState(x => x + 1); + } + React.useEffect( + () => { + ticker(); + }, + [Math.min(state, 4)], + ); + return state; + } + const el = document.createElement('div'); + await act(async () => { + ReactDOM.render(, el); + }); + + // all 5 ticks present and accounted for + expect(el.innerHTML).toBe('5'); + }); + }); + + it('warns if you return a value inside act', () => { + expect(() => act(() => null)).toWarnDev( + [ + 'The callback passed to act(...) function must return undefined, or a Promise.', + ], + {withoutStack: true}, + ); + expect(() => act(() => 123)).toWarnDev( + [ + 'The callback passed to act(...) function must return undefined, or a Promise.', + ], + {withoutStack: true}, + ); + }); + + it('warns if you try to await an .act call', () => { + expect(() => act(() => {}).then(() => {})).toWarnDev( + [ + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ], + {withoutStack: true}, + ); + }); + }); + describe('asynchronous tests', () => { + it('can handle timers', async () => { + function App() { + let [ctr, setCtr] = React.useState(0); + function doSomething() { + setTimeout(() => { + setCtr(1); + }, 50); + } + + React.useEffect(() => { + doSomething(); + }, []); + return ctr; + } + const el = document.createElement('div'); + await act(async () => { + act(() => { + ReactDOM.render(, el); + }); + + await sleep(100); + expect(el.innerHTML).toBe('1'); + }); + }); + + it('can handle async/await', async () => { + function App() { + let [ctr, setCtr] = React.useState(0); + async function someAsyncFunction() { + // queue a bunch of promises to be sure they all flush + await null; + await null; + await null; + setCtr(1); + } + React.useEffect(() => { + someAsyncFunction(); + }, []); + return ctr; + } + const el = document.createElement('div'); + + await act(async () => { + act(() => { + ReactDOM.render(, el); + }); + // pending promises will close before this ends + }); + expect(el.innerHTML).toEqual('1'); + }); + + it('warns if you do not await an act call', async () => { + spyOnDevAndProd(console, 'error'); + act(async () => {}); + // it's annoying that we have to wait a tick before this warning comes in + await sleep(0); + if (__DEV__) { + expect(console.error.calls.count()).toEqual(1); + expect(console.error.calls.argsFor(0)[0]).toMatch( + 'You called act(async () => ...) without await.', + ); + } + }); + + it('warns if you try to interleave multiple act calls', async () => { + spyOnDevAndProd(console, 'error'); + // let's try to cheat and spin off a 'thread' with an act call + (async () => { + await act(async () => { + await sleep(50); + }); + })(); + + await act(async () => { + await sleep(100); + }); + + await sleep(150); + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(1); + } + }); + + it('commits and effects are guaranteed to be flushed', async () => { + function App(props) { + let [state, setState] = React.useState(0); + async function something() { + await null; + setState(1); + } + React.useEffect(() => { + something(); + }, []); + React.useEffect(() => { + props.callback(); + }); + return state; + } + let ctr = 0; + const div = document.createElement('div'); + + await act(async () => { + act(() => { + ReactDOM.render( ctr++} />, div); + }); + expect(div.innerHTML).toBe('0'); + expect(ctr).toBe(1); + }); + // this may seem odd, but it matches user behaviour - + // a flash of "0" followed by "1" + + expect(div.innerHTML).toBe('1'); + expect(ctr).toBe(2); + }); + + it('propagates errors', async () => { + let err; + try { + await act(async () => { + throw new Error('some error'); + }); + } catch (_err) { + err = _err; + } finally { + expect(err instanceof Error).toBe(true); + expect(err.message).toBe('some error'); + } + }); + it('can handle cascading promises', async () => { + // this component triggers an effect, that waits a tick, + // then sets state. repeats this 5 times. + function App() { + let [state, setState] = React.useState(0); + async function ticker() { + await null; + setState(x => x + 1); + } + React.useEffect( + () => { + ticker(); + }, + [Math.min(state, 4)], + ); + return state; + } + const el = document.createElement('div'); + await act(async () => { + ReactDOM.render(, el); + }); + // all 5 ticks present and accounted for + expect(el.innerHTML).toBe('5'); + }); + }); +}); diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index e226843322f7..728f775adf5e 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -35,6 +35,7 @@ import { getPublicRootInstance, findHostInstance, findHostInstanceWithWarning, + flushPassiveEffects, } from 'react-reconciler/inline.dom'; import {createPortal as createPortalImpl} from 'shared/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -807,7 +808,7 @@ const ReactDOM: Object = { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { // Keep in sync with ReactDOMUnstableNativeDependencies.js - // and ReactTestUtils.js. This is an array for better minification. + // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification. Events: [ getInstanceFromNode, getNodeFromInstance, @@ -820,6 +821,7 @@ const ReactDOM: Object = { restoreStateIfNeeded, dispatchEvent, runEventsInBatch, + flushPassiveEffects, ], }, }; diff --git a/packages/react-dom/src/fire/ReactFire.js b/packages/react-dom/src/fire/ReactFire.js index 525f93588225..e4f555ebd627 100644 --- a/packages/react-dom/src/fire/ReactFire.js +++ b/packages/react-dom/src/fire/ReactFire.js @@ -40,6 +40,7 @@ import { getPublicRootInstance, findHostInstance, findHostInstanceWithWarning, + flushPassiveEffects, } from 'react-reconciler/inline.fire'; import {createPortal as createPortalImpl} from 'shared/ReactPortal'; import {canUseDOM} from 'shared/ExecutionEnvironment'; @@ -826,6 +827,7 @@ const ReactDOM: Object = { restoreStateIfNeeded, dispatchEvent, runEventsInBatch, + flushPassiveEffects, ], }, }; diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index 06c39357fb29..f463cc4aa6a3 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -4,6 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -22,15 +23,11 @@ import warningWithoutStack from 'shared/warningWithoutStack'; import {ELEMENT_NODE} from '../shared/HTMLNodeType'; import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes'; import {PLUGIN_EVENT_SYSTEM} from 'events/EventSystemFlags'; - -// for .act's return value -type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): mixed, -}; +import act from './ReactTestUtilsAct'; const {findDOMNode} = ReactDOM; // Keep in sync with ReactDOMUnstableNativeDependencies.js -// and ReactDOM.js: +// ReactDOM.js, and ReactTestUtilsAct.js: const [ getInstanceFromNode, /* eslint-disable no-unused-vars */ @@ -45,6 +42,8 @@ const [ restoreStateIfNeeded, dispatchEvent, runEventsInBatch, + // eslint-disable-next-line no-unused-vars + flushPassiveEffects, ] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events; function Event(suffix) {} @@ -152,9 +151,12 @@ function validateClassInstance(inst, methodName) { ); } -// a stub element, lazily initialized, used by act() when flushing effects +// a plain dom element, lazily initialized, used by act() when flushing effects let actContainerElement = null; +// a warning for when you try to use TestUtils.act in a non-browser environment +let didWarnAboutActInNodejs = false; + /** * Utilities for making it easy to test React components. * @@ -391,57 +393,24 @@ const ReactTestUtils = { Simulate: null, SimulateNative: {}, - act(callback: () => void): Thenable { + act(callback: () => Thenable) { if (actContainerElement === null) { - // warn if we can't actually create the stub element if (__DEV__) { - warningWithoutStack( - typeof document !== 'undefined' && - document !== null && - typeof document.createElement === 'function', - 'It looks like you called TestUtils.act(...) in a non-browser environment. ' + - "If you're using TestRenderer for your tests, you should call " + - 'TestRenderer.act(...) instead of TestUtils.act(...).', - ); - } - // then make it - actContainerElement = document.createElement('div'); - } - - const result = ReactDOM.unstable_batchedUpdates(callback); - // note: keep these warning messages in sync with - // createReactNoop.js and ReactTestRenderer.js - if (__DEV__) { - if (result !== undefined) { - let addendum; - if (result !== null && typeof result.then === 'function') { - addendum = - '\n\nIt looks like you wrote ReactTestUtils.act(async () => ...), ' + - 'or returned a Promise from the callback passed to it. ' + - 'Putting asynchronous logic inside ReactTestUtils.act(...) is not supported.\n'; - } else { - addendum = ' You returned: ' + result; - } - warningWithoutStack( - false, - 'The callback passed to ReactTestUtils.act(...) function must not return anything.%s', - addendum, - ); - } - } - ReactDOM.render(
, actContainerElement); - // we want the user to not expect a return, - // but we want to warn if they use it like they can await on it. - return { - then() { - if (__DEV__) { + // warn if we're trying to use this in something like node (without jsdom) + if (didWarnAboutActInNodejs === false) { + didWarnAboutActInNodejs = true; warningWithoutStack( - false, - 'Do not await the result of calling ReactTestUtils.act(...), it is not a Promise.', + typeof document !== 'undefined' && document !== null, + 'It looks like you called ReactTestUtils.act(...) in a non-browser environment. ' + + "If you're using TestRenderer for your tests, you should call " + + 'ReactTestRenderer.act(...) instead of ReactTestUtils.act(...).', ); } - }, - }; + } + // now make the stub element + actContainerElement = document.createElement('div'); + } + return act(callback); }, }; diff --git a/packages/react-dom/src/test-utils/ReactTestUtilsAct.js b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js new file mode 100644 index 000000000000..99cb73ede7c1 --- /dev/null +++ b/packages/react-dom/src/test-utils/ReactTestUtilsAct.js @@ -0,0 +1,172 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler'; + +import warningWithoutStack from 'shared/warningWithoutStack'; +import ReactDOM from 'react-dom'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import enqueueTask from 'shared/enqueueTask'; + +// Keep in sync with ReactDOMUnstableNativeDependencies.js +// ReactDOM.js, and ReactTestUtils.js: +const [ + /* eslint-disable no-unused-vars */ + getInstanceFromNode, + getNodeFromInstance, + getFiberCurrentPropsFromNode, + injectEventPluginsByName, + eventNameDispatchConfigs, + accumulateTwoPhaseDispatches, + accumulateDirectDispatches, + enqueueStateRestore, + restoreStateIfNeeded, + dispatchEvent, + runEventsInBatch, + /* eslint-enable no-unused-vars */ + flushPassiveEffects, +] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events; + +const batchedUpdates = ReactDOM.unstable_batchedUpdates; + +const {ReactShouldWarnActingUpdates} = ReactSharedInternals; + +// this implementation should be exactly the same in +// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js + +// we track the 'depth' of the act() calls with this counter, +// so we can tell if any async act() calls try to run in parallel. +let actingUpdatesScopeDepth = 0; + +function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { + try { + flushPassiveEffects(); + enqueueTask(() => { + if (flushPassiveEffects()) { + flushEffectsAndMicroTasks(onDone); + } else { + onDone(); + } + }); + } catch (err) { + onDone(err); + } +} + +function act(callback: () => Thenable) { + let previousActingUpdatesScopeDepth; + if (__DEV__) { + previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; + actingUpdatesScopeDepth++; + ReactShouldWarnActingUpdates.current = true; + } + + function onDone() { + if (__DEV__) { + actingUpdatesScopeDepth--; + if (actingUpdatesScopeDepth === 0) { + ReactShouldWarnActingUpdates.current = false; + } + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned + warningWithoutStack( + null, + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + } + } + + const result = batchedUpdates(callback); + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // setup a boolean that gets set to true only + // once this act() call is await-ed + let called = false; + if (__DEV__) { + if (typeof Promise !== 'undefined') { + //eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (called === false) { + warningWithoutStack( + null, + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + + 'calls and mixing their scopes. You should - await act(async () => ...);', + ); + } + }); + } + } + + // in the async case, the returned thenable runs the callback, flushes + // effects and microtasks in a loop until flushPassiveEffects() === false, + // and cleans up + return { + then(resolve: () => void, reject: (?Error) => void) { + called = true; + result.then( + () => { + flushEffectsAndMicroTasks((err: ?Error) => { + onDone(); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, + err => { + onDone(); + reject(err); + }, + ); + }, + }; + } else { + if (__DEV__) { + warningWithoutStack( + result === undefined, + 'The callback passed to act(...) function ' + + 'must return undefined, or a Promise. You returned %s', + result, + ); + } + + // flush effects until none remain, and cleanup + try { + while (flushPassiveEffects()) {} + onDone(); + } catch (err) { + onDone(); + throw err; + } + + // in the sync case, the returned thenable only warns *if* await-ed + return { + then(resolve: () => void) { + if (__DEV__) { + warningWithoutStack( + false, + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ); + } + resolve(); + }, + }; + } +} + +export default act; diff --git a/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js b/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js index f382613ccce7..d53daa4eda10 100644 --- a/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js +++ b/packages/react-dom/src/unstable-native-dependencies/ReactDOMUnstableNativeDependencies.js @@ -11,7 +11,7 @@ import ResponderEventPlugin from 'events/ResponderEventPlugin'; import ResponderTouchHistoryStore from 'events/ResponderTouchHistoryStore'; // Inject react-dom's ComponentTree into this module. -// Keep in sync with ReactDOM.js and ReactTestUtils.js: +// Keep in sync with ReactDOM.js, ReactTestUtils.js, and ReactTestUtilsAct.js: const [ getInstanceFromNode, getNodeFromInstance, diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 8976372bf6c4..55198b6ba43a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -14,6 +14,7 @@ * environment. */ +import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {UpdateQueue} from 'react-reconciler/src/ReactUpdateQueue'; import type {ReactNodeList} from 'shared/ReactTypes'; @@ -26,16 +27,12 @@ import { REACT_ELEMENT_TYPE, REACT_EVENT_TARGET_TOUCH_HIT, } from 'shared/ReactSymbols'; -import warningWithoutStack from 'shared/warningWithoutStack'; import warning from 'shared/warning'; - +import enqueueTask from 'shared/enqueueTask'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import warningWithoutStack from 'shared/warningWithoutStack'; import {enableEventAPI} from 'shared/ReactFeatureFlags'; -// for .act's return value -type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): mixed, -}; - type Container = { rootID: string, children: Array, @@ -59,6 +56,8 @@ type TextInstance = {| |}; type HostContext = Object; +const {ReactShouldWarnActingUpdates} = ReactSharedInternals; + const NO_CONTEXT = {}; const UPPERCASE_CONTEXT = {}; const EVENT_COMPONENT_CONTEXT = {}; @@ -598,6 +597,140 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { const roots = new Map(); const DEFAULT_ROOT_ID = ''; + const {flushPassiveEffects, batchedUpdates} = NoopRenderer; + + // this act() implementation should be exactly the same in + // ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js + + let actingUpdatesScopeDepth = 0; + + function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { + try { + flushPassiveEffects(); + enqueueTask(() => { + if (flushPassiveEffects()) { + flushEffectsAndMicroTasks(onDone); + } else { + onDone(); + } + }); + } catch (err) { + onDone(err); + } + } + + function act(callback: () => Thenable) { + let previousActingUpdatesScopeDepth; + if (__DEV__) { + previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; + actingUpdatesScopeDepth++; + ReactShouldWarnActingUpdates.current = true; + } + + function onDone() { + if (__DEV__) { + actingUpdatesScopeDepth--; + if (actingUpdatesScopeDepth === 0) { + ReactShouldWarnActingUpdates.current = false; + } + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned + warningWithoutStack( + null, + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + } + } + + const result = batchedUpdates(callback); + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // setup a boolean that gets set to true only + // once this act() call is await-ed + let called = false; + if (__DEV__) { + if (typeof Promise !== 'undefined') { + //eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (called === false) { + warningWithoutStack( + null, + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + + 'calls and mixing their scopes. You should - await act(async () => ...);', + ); + } + }); + } + } + + // in the async case, the returned thenable runs the callback, flushes + // effects and microtasks in a loop until flushPassiveEffects() === false, + // and cleans up + return { + then(resolve: () => void, reject: (?Error) => void) { + called = true; + result.then( + () => { + flushEffectsAndMicroTasks((err: ?Error) => { + onDone(); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, + err => { + onDone(); + reject(err); + }, + ); + }, + }; + } else { + if (__DEV__) { + warningWithoutStack( + result === undefined, + 'The callback passed to act(...) function ' + + 'must return undefined, or a Promise. You returned %s', + result, + ); + } + + // flush effects until none remain, and cleanup + try { + while (flushPassiveEffects()) {} + onDone(); + } catch (err) { + onDone(); + throw err; + } + + // in the sync case, the returned thenable only warns *if* await-ed + return { + then(resolve: () => void) { + if (__DEV__) { + warningWithoutStack( + false, + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ); + } + resolve(); + }, + }; + } + } + + // end act() implementation + function childToJSX(child, text) { if (text !== null) { return text; @@ -843,56 +976,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { interactiveUpdates: NoopRenderer.interactiveUpdates, - // maybe this should exist only in the test file - act(callback: () => void): Thenable { - // note: keep these warning messages in sync with - // ReactTestRenderer.js and ReactTestUtils.js - let result = NoopRenderer.batchedUpdates(callback); - if (__DEV__) { - if (result !== undefined) { - let addendum; - if (result !== null && typeof result.then === 'function') { - addendum = - "\n\nIt looks like you wrote ReactNoop.act(async () => ...) or returned a Promise from it's callback. " + - 'Putting asynchronous logic inside ReactNoop.act(...) is not supported.\n'; - } else { - addendum = ' You returned: ' + result; - } - warningWithoutStack( - false, - 'The callback passed to ReactNoop.act(...) function must not return anything.%s', - addendum, - ); - } - } - ReactNoop.flushPassiveEffects(); - // we want the user to not expect a return, - // but we want to warn if they use it like they can await on it. - return { - then() { - if (__DEV__) { - warningWithoutStack( - false, - 'Do not await the result of calling ReactNoop.act(...), it is not a Promise.', - ); - } - }, - }; - }, - flushSync(fn: () => mixed) { NoopRenderer.flushSync(fn); }, - flushPassiveEffects() { - // Trick to flush passive effects without exposing an internal API: - // Create a throwaway root and schedule a dummy update on it. - const rootID = 'bloopandthenmoreletterstoavoidaconflict'; - const container = {rootID: rootID, pendingChildren: [], children: []}; - rootContainers.set(rootID, container); - const root = NoopRenderer.createContainer(container, true, false); - NoopRenderer.updateContainer(null, root, null, null); - }, + flushPassiveEffects: NoopRenderer.flushPassiveEffects, + + act, // Logs the current state of the tree. dumpTree(rootID: string = DEFAULT_ROOT_ID) { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index 57d8a7c4edb7..80707120e95f 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -30,10 +30,10 @@ import { } from './ReactHookEffectTags'; import { scheduleWork, - warnIfNotCurrentlyBatchingInDev, computeExpirationForFiber, flushPassiveEffects, requestCurrentTime, + warnIfNotCurrentlyActingUpdatesInDev, } from './ReactFiberScheduler'; import invariant from 'shared/invariant'; @@ -1046,19 +1046,6 @@ function updateMemo( return nextValue; } -// in a test-like environment, we want to warn if dispatchAction() -// is called outside of a batchedUpdates/TestUtils.act(...) call. -let shouldWarnForUnbatchedSetState = false; - -if (__DEV__) { - // jest isn't a 'global', it's just exposed to tests via a wrapped function - // further, this isn't a test file, so flow doesn't recognize the symbol. So... - // $FlowExpectedError - because requirements don't give a damn about your type sigs. - if ('undefined' !== typeof jest) { - shouldWarnForUnbatchedSetState = true; - } -} - function dispatchAction( fiber: Fiber, queue: UpdateQueue, @@ -1178,8 +1165,11 @@ function dispatchAction( } } if (__DEV__) { - if (shouldWarnForUnbatchedSetState === true) { - warnIfNotCurrentlyBatchingInDev(fiber); + // jest isn't a 'global', it's just exposed to tests via a wrapped function + // further, this isn't a test file, so flow doesn't recognize the symbol. So... + // $FlowExpectedError - because requirements don't give a damn about your type sigs. + if ('undefined' !== typeof jest) { + warnIfNotCurrentlyActingUpdatesInDev(fiber); } } scheduleWork(fiber, expirationTime); diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 01e303c74bc4..eb98b19ce4f0 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -310,6 +310,7 @@ export { flushInteractiveUpdates, flushControlled, flushSync, + flushPassiveEffects, }; export function getPublicRootInstance( diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 5bea74a78b5f..df179b766e15 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -34,7 +34,7 @@ import { flushInteractiveUpdates as flushInteractiveUpdates_old, computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_old, flushPassiveEffects as flushPassiveEffects_old, - warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_old, + warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_old, } from './ReactFiberScheduler.old'; import { @@ -62,7 +62,7 @@ import { flushInteractiveUpdates as flushInteractiveUpdates_new, computeUniqueAsyncExpiration as computeUniqueAsyncExpiration_new, flushPassiveEffects as flushPassiveEffects_new, - warnIfNotCurrentlyBatchingInDev as warnIfNotCurrentlyBatchingInDev_new, + warnIfNotCurrentlyActingUpdatesInDev as warnIfNotCurrentlyActingUpdatesInDev_new, } from './ReactFiberScheduler.new'; export let requestCurrentTime = requestCurrentTime_old; @@ -89,7 +89,7 @@ export let interactiveUpdates = interactiveUpdates_old; export let flushInteractiveUpdates = flushInteractiveUpdates_old; export let computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_old; export let flushPassiveEffects = flushPassiveEffects_old; -export let warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_old; +export let warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_old; if (enableNewScheduler) { requestCurrentTime = requestCurrentTime_new; @@ -116,9 +116,9 @@ if (enableNewScheduler) { flushInteractiveUpdates = flushInteractiveUpdates_new; computeUniqueAsyncExpiration = computeUniqueAsyncExpiration_new; flushPassiveEffects = flushPassiveEffects_new; - warnIfNotCurrentlyBatchingInDev = warnIfNotCurrentlyBatchingInDev_new; + warnIfNotCurrentlyActingUpdatesInDev = warnIfNotCurrentlyActingUpdatesInDev_new; } export type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): mixed, + then(resolve: () => mixed, reject?: () => mixed): void | Thenable, }; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.new.js b/packages/react-reconciler/src/ReactFiberScheduler.new.js index e4df9dad4fe7..810cbd8847d8 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.new.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.new.js @@ -33,4 +33,4 @@ export const interactiveUpdates = notYetImplemented; export const flushInteractiveUpdates = notYetImplemented; export const computeUniqueAsyncExpiration = notYetImplemented; export const flushPassiveEffects = notYetImplemented; -export const warnIfNotCurrentlyBatchingInDev = notYetImplemented; +export const warnIfNotCurrentlyActingUpdatesInDev = notYetImplemented; diff --git a/packages/react-reconciler/src/ReactFiberScheduler.old.js b/packages/react-reconciler/src/ReactFiberScheduler.old.js index 887305457f1d..1a4b15b2bee9 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.old.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.old.js @@ -176,10 +176,14 @@ const { } = Scheduler; export type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): mixed, + then(resolve: () => mixed, reject?: () => mixed): void | Thenable, }; -const {ReactCurrentDispatcher, ReactCurrentOwner} = ReactSharedInternals; +const { + ReactCurrentDispatcher, + ReactCurrentOwner, + ReactShouldWarnActingUpdates, +} = ReactSharedInternals; let didWarnAboutStateTransition; let didWarnSetStateChildContext; @@ -610,6 +614,7 @@ function markLegacyErrorBoundaryAsFailed(instance: mixed) { } function flushPassiveEffects() { + const didFlushEffects = passiveEffectCallback !== null; if (passiveEffectCallbackHandle !== null) { cancelCallback(passiveEffectCallbackHandle); } @@ -618,6 +623,7 @@ function flushPassiveEffects() { // to ensure tracing works correctly. passiveEffectCallback(); } + return didFlushEffects; } function commitRoot(root: FiberRoot, finishedWork: Fiber): void { @@ -1836,9 +1842,20 @@ function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null { return root; } -export function warnIfNotCurrentlyBatchingInDev(fiber: Fiber): void { +// in a test-like environment, we want to warn if dispatchAction() is +// called outside of a TestUtils.act(...)/batchedUpdates/render call. +// so we have a a step counter for when we descend/ascend from +// act() calls, and test on it for when to warn +// It's a tuple with a single value. Look for shared/createAct to +// see how we change the value inside act() calls + +export function warnIfNotCurrentlyActingUpdatesInDev(fiber: Fiber): void { if (__DEV__) { - if (isRendering === false && isBatchingUpdates === false) { + if ( + isBatchingUpdates === false && + isRendering === false && + ReactShouldWarnActingUpdates.current === false + ) { warningWithoutStack( false, 'An update to %s inside a test was not wrapped in act(...).\n\n' + diff --git a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js index 455df5d55706..f3f00658b5d9 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooks-test.internal.js @@ -1046,11 +1046,10 @@ describe('ReactHooks', () => { class Cls extends React.Component { render() { - act(() => - _setState(() => { - ReactCurrentDispatcher.current.readContext(ThemeContext); - }), + _setState(() => + ReactCurrentDispatcher.current.readContext(ThemeContext), ); + return null; } } @@ -1062,13 +1061,7 @@ describe('ReactHooks', () => { , ), - ).toWarnDev( - [ - 'Context can only be read while React is rendering', - 'Render methods should be a pure function of props and state', - ], - {withoutStack: 1}, - ); + ).toWarnDev(['Context can only be read while React is rendering']); }); it('warns when calling hooks inside useReducer', () => { diff --git a/packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js b/packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js new file mode 100644 index 000000000000..f3aba9b99881 --- /dev/null +++ b/packages/react-reconciler/src/__tests__/ReactNoopRendererAct-test.js @@ -0,0 +1,63 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @jest-environment node + */ + +// sanity tests for ReactNoop.act() + +jest.useRealTimers(); +const React = require('react'); +const ReactNoop = require('react-noop-renderer'); +const Scheduler = require('scheduler'); + +describe('ReactNoop.act()', () => { + it('can use act to flush effects', async () => { + function App(props) { + React.useEffect(props.callback); + return null; + } + + let calledLog = []; + ReactNoop.act(() => { + ReactNoop.render( + { + calledLog.push(calledLog.length); + }} + />, + ); + }); + expect(Scheduler).toFlushWithoutYielding(); + expect(calledLog).toEqual([0]); + }); + + it('should work with async/await', async () => { + function App() { + let [ctr, setCtr] = React.useState(0); + async function someAsyncFunction() { + Scheduler.yieldValue('stage 1'); + await null; + Scheduler.yieldValue('stage 2'); + setCtr(1); + } + React.useEffect(() => { + someAsyncFunction(); + }, []); + return ctr; + } + await ReactNoop.act(async () => { + ReactNoop.act(() => { + ReactNoop.render(); + }); + await null; + expect(Scheduler).toFlushAndYield(['stage 1']); + }); + expect(Scheduler).toHaveYielded(['stage 2']); + expect(Scheduler).toFlushWithoutYielding(); + expect(ReactNoop.getChildren()).toEqual([{text: '1', hidden: false}]); + }); +}); diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js index de53adb20193..20553ba82191 100644 --- a/packages/react-test-renderer/src/ReactTestRenderer.js +++ b/packages/react-test-renderer/src/ReactTestRenderer.js @@ -40,7 +40,7 @@ import { } from 'shared/ReactWorkTags'; import invariant from 'shared/invariant'; import ReactVersion from 'shared/ReactVersion'; -import warningWithoutStack from 'shared/warningWithoutStack'; +import act from './ReactTestRendererAct'; import {getPublicInstance} from './ReactTestHostConfig'; @@ -65,11 +65,6 @@ type FindOptions = $Shape<{ export type Predicate = (node: ReactTestInstance) => ?boolean; -// for .act's return value -type Thenable = { - then(resolve: () => mixed, reject?: () => mixed): mixed, -}; - const defaultTestOptions = { createNodeMock: function() { return null; @@ -549,63 +544,11 @@ const ReactTestRendererFiber = { return entry; }, - /* eslint-disable camelcase */ + /* eslint-disable-next-line camelcase */ unstable_batchedUpdates: batchedUpdates, - /* eslint-enable camelcase */ - - act(callback: () => void): Thenable { - // note: keep these warning messages in sync with - // createNoop.js and ReactTestUtils.js - let result = batchedUpdates(callback); - if (__DEV__) { - if (result !== undefined) { - let addendum; - if (result !== null && typeof result.then === 'function') { - addendum = - "\n\nIt looks like you wrote TestRenderer.act(async () => ...) or returned a Promise from it's callback. " + - 'Putting asynchronous logic inside TestRenderer.act(...) is not supported.\n'; - } else { - addendum = ' You returned: ' + result; - } - warningWithoutStack( - false, - 'The callback passed to TestRenderer.act(...) function must not return anything.%s', - addendum, - ); - } - } - flushPassiveEffects(); - // we want the user to not expect a return, - // but we want to warn if they use it like they can await on it. - return { - then() { - if (__DEV__) { - warningWithoutStack( - false, - 'Do not await the result of calling TestRenderer.act(...), it is not a Promise.', - ); - } - }, - }; - }, -}; -// root used to flush effects during .act() calls -const actRoot = createContainer( - { - children: [], - createNodeMock: defaultTestOptions.createNodeMock, - tag: 'CONTAINER', - }, - true, - false, -); - -function flushPassiveEffects() { - // Trick to flush passive effects without exposing an internal API: - // Create a throwaway root and schedule a dummy update on it. - updateContainer(null, actRoot, null, null); -} + act, +}; const fiberToWrapper = new WeakMap(); function wrapFiber(fiber: Fiber): ReactTestInstance { diff --git a/packages/react-test-renderer/src/ReactTestRendererAct.js b/packages/react-test-renderer/src/ReactTestRendererAct.js new file mode 100644 index 000000000000..37ced3fb04c0 --- /dev/null +++ b/packages/react-test-renderer/src/ReactTestRendererAct.js @@ -0,0 +1,153 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ +import type {Thenable} from 'react-reconciler/src/ReactFiberScheduler'; + +import { + batchedUpdates, + flushPassiveEffects, +} from 'react-reconciler/inline.test'; +import ReactSharedInternals from 'shared/ReactSharedInternals'; +import warningWithoutStack from 'shared/warningWithoutStack'; +import enqueueTask from 'shared/enqueueTask'; + +const {ReactShouldWarnActingUpdates} = ReactSharedInternals; + +// this implementation should be exactly the same in +// ReactTestUtilsAct.js, ReactTestRendererAct.js, createReactNoop.js + +// we track the 'depth' of the act() calls with this counter, +// so we can tell if any async act() calls try to run in parallel. +let actingUpdatesScopeDepth = 0; + +function flushEffectsAndMicroTasks(onDone: (err: ?Error) => void) { + try { + flushPassiveEffects(); + enqueueTask(() => { + if (flushPassiveEffects()) { + flushEffectsAndMicroTasks(onDone); + } else { + onDone(); + } + }); + } catch (err) { + onDone(err); + } +} + +function act(callback: () => Thenable) { + let previousActingUpdatesScopeDepth; + if (__DEV__) { + previousActingUpdatesScopeDepth = actingUpdatesScopeDepth; + actingUpdatesScopeDepth++; + ReactShouldWarnActingUpdates.current = true; + } + + function onDone() { + if (__DEV__) { + actingUpdatesScopeDepth--; + if (actingUpdatesScopeDepth === 0) { + ReactShouldWarnActingUpdates.current = false; + } + if (actingUpdatesScopeDepth > previousActingUpdatesScopeDepth) { + // if it's _less than_ previousActingUpdatesScopeDepth, then we can assume the 'other' one has warned + warningWithoutStack( + null, + 'You seem to have overlapping act() calls, this is not supported. ' + + 'Be sure to await previous act() calls before making a new one. ', + ); + } + } + } + + const result = batchedUpdates(callback); + if ( + result !== null && + typeof result === 'object' && + typeof result.then === 'function' + ) { + // setup a boolean that gets set to true only + // once this act() call is await-ed + let called = false; + if (__DEV__) { + if (typeof Promise !== 'undefined') { + //eslint-disable-next-line no-undef + Promise.resolve() + .then(() => {}) + .then(() => { + if (called === false) { + warningWithoutStack( + null, + 'You called act(async () => ...) without await. ' + + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + + 'calls and mixing their scopes. You should - await act(async () => ...);', + ); + } + }); + } + } + + // in the async case, the returned thenable runs the callback, flushes + // effects and microtasks in a loop until flushPassiveEffects() === false, + // and cleans up + return { + then(resolve: () => void, reject: (?Error) => void) { + called = true; + result.then( + () => { + flushEffectsAndMicroTasks((err: ?Error) => { + onDone(); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }, + err => { + onDone(); + reject(err); + }, + ); + }, + }; + } else { + if (__DEV__) { + warningWithoutStack( + result === undefined, + 'The callback passed to act(...) function ' + + 'must return undefined, or a Promise. You returned %s', + result, + ); + } + + // flush effects until none remain, and cleanup + try { + while (flushPassiveEffects()) {} + onDone(); + } catch (err) { + onDone(); + throw err; + } + + // in the sync case, the returned thenable only warns *if* await-ed + return { + then(resolve: () => void) { + if (__DEV__) { + warningWithoutStack( + false, + 'Do not await the result of calling act(...) with sync logic, it is not a Promise.', + ); + } + resolve(); + }, + }; + } +} + +export default act; diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js index 0f7690e671f3..37820febaf67 100644 --- a/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js +++ b/packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js @@ -1023,40 +1023,19 @@ describe('ReactTestRenderer', () => { ReactTestRenderer.create(); }); - describe('act', () => { - it('can use .act() to batch updates and effects', () => { - function App(props) { - React.useEffect(() => { - props.callback(); - }); - return null; - } - let called = false; - ReactTestRenderer.act(() => { - ReactTestRenderer.create( - { - called = true; - }} - />, - ); - }); - - expect(called).toBe(true); - }); - it('warns and throws if you use TestUtils.act instead of TestRenderer.act in node', () => { - // we warn when you try to load 2 renderers in the same 'scope' - // so as suggested, we call resetModules() to carry on with the test - jest.resetModules(); - const {act} = require('react-dom/test-utils'); - expect(() => { - expect(() => act(() => {})).toThrow('document is not defined'); - }).toWarnDev( - [ - 'It looks like you called TestUtils.act(...) in a non-browser environment', - ], - {withoutStack: 1}, - ); - }); + // we run this test here because we need a dom-less scope + it('warns and throws if you use TestUtils.act instead of TestRenderer.act in node', () => { + // we warn when you try to load 2 renderers in the same 'scope' + // so as suggested, we call resetModules() to carry on with the test + jest.resetModules(); + const {act} = require('react-dom/test-utils'); + expect(() => { + expect(() => act(() => {})).toThrow('document is not defined'); + }).toWarnDev( + [ + 'It looks like you called ReactTestUtils.act(...) in a non-browser environment', + ], + {withoutStack: 1}, + ); }); }); diff --git a/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js new file mode 100644 index 000000000000..9390c4ccfc74 --- /dev/null +++ b/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js @@ -0,0 +1,122 @@ +jest.useRealTimers(); + +let React; +let ReactTestRenderer; +let Scheduler; +let act; + +describe('ReactTestRenderer.act()', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactTestRenderer = require('react-test-renderer'); + Scheduler = require('scheduler'); + act = ReactTestRenderer.act; + }); + it('can use .act() to flush effects', () => { + function App(props) { + let [ctr, setCtr] = React.useState(0); + React.useEffect(() => { + props.callback(); + setCtr(1); + }, []); + return ctr; + } + let calledLog = []; + let root; + act(() => { + root = ReactTestRenderer.create( + { + calledLog.push(calledLog.length); + }} + />, + ); + }); + + expect(calledLog).toEqual([0]); + expect(root.toJSON()).toEqual('1'); + }); + + it("warns if you don't use .act", () => { + let setCtr; + function App(props) { + let [ctr, _setCtr] = React.useState(0); + setCtr = _setCtr; + return ctr; + } + + ReactTestRenderer.create(); + + expect(() => { + setCtr(1); + }).toWarnDev([ + 'An update to App inside a test was not wrapped in act(...)', + ]); + }); + + describe('async', () => { + it('should work with async/await', async () => { + function fetch(url) { + return Promise.resolve({ + details: [1, 2, 3], + }); + } + function App() { + let [details, setDetails] = React.useState(0); + + React.useEffect(() => { + async function fetchDetails() { + const response = await fetch(); + setDetails(response.details); + } + fetchDetails(); + }, []); + return details; + } + let root; + + await ReactTestRenderer.act(async () => { + root = ReactTestRenderer.create(); + }); + + expect(root.toJSON()).toEqual(['1', '2', '3']); + }); + + it('should not flush effects without also flushing microtasks', async () => { + const {useEffect, useReducer} = React; + + const alreadyResolvedPromise = Promise.resolve(); + + function App() { + // This component will keep updating itself until step === 3 + const [step, proceed] = useReducer(s => (s === 3 ? 3 : s + 1), 1); + useEffect(() => { + Scheduler.yieldValue('Effect'); + alreadyResolvedPromise.then(() => { + Scheduler.yieldValue('Microtask'); + proceed(); + }); + }); + return step; + } + const root = ReactTestRenderer.create(null); + await act(async () => { + root.update(); + }); + expect(Scheduler).toHaveYielded([ + // Should not flush effects without also flushing microtasks + // First render: + 'Effect', + 'Microtask', + // Second render: + 'Effect', + 'Microtask', + // Final render: + 'Effect', + 'Microtask', + ]); + expect(root).toMatchRenderedOutput('3'); + }); + }); +}); diff --git a/packages/react/src/ReactSharedInternals.js b/packages/react/src/ReactSharedInternals.js index 53dc43d977e5..c6d5010cb977 100644 --- a/packages/react/src/ReactSharedInternals.js +++ b/packages/react/src/ReactSharedInternals.js @@ -15,6 +15,8 @@ import ReactDebugCurrentFrame from './ReactDebugCurrentFrame'; const ReactSharedInternals = { ReactCurrentDispatcher, ReactCurrentOwner, + // used by act() + ReactShouldWarnActingUpdates: {current: false}, // Used by renderers to avoid bundling object-assign twice in UMD bundles: assign, }; diff --git a/packages/shared/enqueueTask.js b/packages/shared/enqueueTask.js new file mode 100644 index 000000000000..02f4ee220672 --- /dev/null +++ b/packages/shared/enqueueTask.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import warningWithoutStack from './warningWithoutStack'; + +let didWarnAboutMessageChannel = false; +let enqueueTask; +try { + // assuming we're in node, let's try to get node's + // version of setImmediate, bypassing fake timers if any + let r = require; // trick packagers not to bundle this stuff. + enqueueTask = r('timers').setImmediate; +} catch (_err) { + // we're in a browser + // we can't use regular timers because they may still be faked + // so we try MessageChannel+postMessage instead + enqueueTask = function(callback: () => void) { + if (__DEV__) { + if (didWarnAboutMessageChannel === false) { + didWarnAboutMessageChannel = true; + warningWithoutStack( + typeof MessageChannel !== 'undefined', + 'This browser does not have a MessageChannel implementation, ' + + 'so enqueuing tasks via await act(async () => ...) will fail. ' + + 'Please file an issue at https://github.com/facebook/react/issues ' + + 'if you encounter this warning.', + ); + } + } + const channel = new MessageChannel(); + channel.port1.onmessage = callback; + channel.port2.postMessage(undefined); + }; +} + +export default enqueueTask; diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index ddb87f522b2e..c858da46fff8 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -4,29 +4,29 @@ "filename": "react.development.js", "bundleType": "UMD_DEV", "packageName": "react", - "size": 102492, - "gzip": 26625 + "size": 103860, + "gzip": 26948 }, { "filename": "react.production.min.js", "bundleType": "UMD_PROD", "packageName": "react", - "size": 12609, - "gzip": 4834 + "size": 12346, + "gzip": 4735 }, { "filename": "react.development.js", "bundleType": "NODE_DEV", "packageName": "react", - "size": 64139, - "gzip": 17318 + "size": 65507, + "gzip": 17628 }, { "filename": "react.production.min.js", "bundleType": "NODE_PROD", "packageName": "react", - "size": 6834, - "gzip": 2814 + "size": 6571, + "gzip": 2718 }, { "filename": "React-dev.js", @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 832345, - "gzip": 188603 + "size": 835612, + "gzip": 189016 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 107683, - "gzip": 34867 + "size": 107684, + "gzip": 34777 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 826372, - "gzip": 186963 + "size": 829867, + "gzip": 187373 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", "size": 107664, - "gzip": 34301 + "gzip": 34273 }, { "filename": "ReactDOM-dev.js", @@ -88,29 +88,29 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 48620, - "gzip": 13278 + "size": 53238, + "gzip": 14410 }, { "filename": "react-dom-test-utils.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 10184, - "gzip": 3732 + "size": 10659, + "gzip": 3913 }, { "filename": "react-dom-test-utils.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 48334, - "gzip": 13206 + "size": 52952, + "gzip": 14343 }, { "filename": "react-dom-test-utils.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 9954, - "gzip": 3660 + "size": 10441, + "gzip": 3835 }, { "filename": "ReactTestUtils-dev.js", @@ -123,8 +123,8 @@ "filename": "react-dom-unstable-native-dependencies.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 62190, - "gzip": 16206 + "size": 62213, + "gzip": 16213 }, { "filename": "react-dom-unstable-native-dependencies.production.min.js", @@ -137,8 +137,8 @@ "filename": "react-dom-unstable-native-dependencies.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 61854, - "gzip": 16078 + "size": 61877, + "gzip": 16085 }, { "filename": "react-dom-unstable-native-dependencies.production.min.js", @@ -165,29 +165,29 @@ "filename": "react-dom-server.browser.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 136840, - "gzip": 36205 + "size": 137930, + "gzip": 36351 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 19363, - "gzip": 7290 + "size": 19569, + "gzip": 7381 }, { "filename": "react-dom-server.browser.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 132878, - "gzip": 35237 + "size": 133968, + "gzip": 35384 }, { "filename": "react-dom-server.browser.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 19287, - "gzip": 7290 + "size": 19495, + "gzip": 7377 }, { "filename": "ReactDOMServer-dev.js", @@ -207,43 +207,43 @@ "filename": "react-dom-server.node.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 134867, - "gzip": 35791 + "size": 135957, + "gzip": 35941 }, { "filename": "react-dom-server.node.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 20170, - "gzip": 7598 + "size": 20377, + "gzip": 7690 }, { "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 570224, - "gzip": 123819 + "size": 582400, + "gzip": 125635 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 99370, - "gzip": 30598 + "size": 99129, + "gzip": 30380 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 499479, - "gzip": 106084 + "size": 511655, + "gzip": 107868 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 63450, - "gzip": 19455 + "size": 63295, + "gzip": 19346 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 508633, - "gzip": 107760 + "size": 525649, + "gzip": 110641 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 64479, - "gzip": 19791 + "size": 64570, + "gzip": 19714 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 504043, - "gzip": 106587 + "size": 521059, + "gzip": 109459 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 64119, - "gzip": 19538 + "size": 64265, + "gzip": 19537 }, { "filename": "ReactTestRenderer-dev.js", @@ -326,29 +326,29 @@ "filename": "react-test-renderer-shallow.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 38113, - "gzip": 9733 + "size": 39907, + "gzip": 10032 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 11385, - "gzip": 3419 + "size": 11688, + "gzip": 3579 }, { "filename": "react-test-renderer-shallow.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 32275, - "gzip": 8333 + "size": 33992, + "gzip": 8604 }, { "filename": "react-test-renderer-shallow.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 12046, - "gzip": 3733 + "size": 11884, + "gzip": 3709 }, { "filename": "ReactShallowRenderer-dev.js", @@ -361,57 +361,57 @@ "filename": "react-noop-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 26949, - "gzip": 6362 + "size": 35270, + "gzip": 8615 }, { "filename": "react-noop-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 9933, - "gzip": 3165 + "size": 10470, + "gzip": 3431 }, { "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 497879, - "gzip": 104645 + "size": 511941, + "gzip": 106665 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 64551, - "gzip": 19325 + "size": 64562, + "gzip": 19196 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 495898, - "gzip": 103848 + "size": 509778, + "gzip": 105781 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 64562, - "gzip": 19330 + "size": 64573, + "gzip": 19202 }, { "filename": "react-reconciler-reflection.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", "size": 16161, - "gzip": 5096 + "gzip": 5015 }, { "filename": "react-reconciler-reflection.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 2760, - "gzip": 1244 + "size": 2423, + "gzip": 1082 }, { "filename": "react-call-return.development.js", @@ -487,57 +487,57 @@ "filename": "create-subscription.development.js", "bundleType": "NODE_DEV", "packageName": "create-subscription", - "size": 8538, - "gzip": 2952 + "size": 8219, + "gzip": 2827 }, { "filename": "create-subscription.production.min.js", "bundleType": "NODE_PROD", "packageName": "create-subscription", - "size": 2889, - "gzip": 1344 + "size": 2558, + "gzip": 1200 }, { "filename": "React-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react", - "size": 61163, - "gzip": 16239 + "size": 63801, + "gzip": 16915 }, { "filename": "React-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react", - "size": 15734, - "gzip": 4189 + "size": 15699, + "gzip": 4193 }, { "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 851672, - "gzip": 188651 + "size": 855350, + "gzip": 189054 }, { "filename": "ReactDOM-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 339041, - "gzip": 62418 + "size": 339926, + "gzip": 62708 }, { "filename": "ReactTestUtils-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 46251, - "gzip": 12476 + "size": 51170, + "gzip": 13698 }, { "filename": "ReactDOMUnstableNativeDependencies-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 60296, - "gzip": 15251 + "size": 60319, + "gzip": 15257 }, { "filename": "ReactDOMUnstableNativeDependencies-prod.js", @@ -550,99 +550,99 @@ "filename": "ReactDOMServer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 135272, - "gzip": 35002 + "size": 136328, + "gzip": 35148 }, { "filename": "ReactDOMServer-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 46877, - "gzip": 10879 + "size": 47596, + "gzip": 10982 }, { "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 509037, - "gzip": 105217 + "size": 521820, + "gzip": 107152 }, { "filename": "ReactART-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-art", - "size": 201874, - "gzip": 34245 + "size": 202109, + "gzip": 34176 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 637829, - "gzip": 136280 + "size": 645983, + "gzip": 137694 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 251569, - "gzip": 43973 + "size": 252030, + "gzip": 44064 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 637742, - "gzip": 136246 + "size": 645895, + "gzip": 137660 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 251583, - "gzip": 43970 + "size": 252044, + "gzip": 44061 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 628028, - "gzip": 133874 + "size": 634566, + "gzip": 134983 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 245497, - "gzip": 42746 + "size": 245276, + "gzip": 42773 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 627933, - "gzip": 133834 + "size": 634470, + "gzip": 134930 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 245503, - "gzip": 42741 + "size": 245282, + "gzip": 42767 }, { "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 514514, - "gzip": 106185 + "size": 532823, + "gzip": 109328 }, { "filename": "ReactShallowRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 30652, - "gzip": 7761 + "size": 33767, + "gzip": 8435 }, { "filename": "ReactIs-dev.js", @@ -704,36 +704,36 @@ "filename": "react-noop-renderer-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-noop-renderer", - "size": 27068, - "gzip": 6375 + "size": 35389, + "gzip": 8630 }, { "filename": "react-noop-renderer-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-noop-renderer", - "size": 9955, - "gzip": 3170 + "size": 10492, + "gzip": 3436 }, { "filename": "react-dom.profiling.min.js", "bundleType": "NODE_PROFILING", "packageName": "react-dom", - "size": 110839, - "gzip": 34944 + "size": 110841, + "gzip": 34901 }, { "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 257964, - "gzip": 45359 + "size": 258447, + "gzip": 45443 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_OSS_PROFILING", "packageName": "react-native-renderer", - "size": 250954, - "gzip": 44143 + "size": 250755, + "gzip": 44122 }, { "filename": "Scheduler-dev.js", @@ -760,50 +760,50 @@ "filename": "React-profiling.js", "bundleType": "FB_WWW_PROFILING", "packageName": "react", - "size": 15734, - "gzip": 4189 + "size": 15699, + "gzip": 4193 }, { "filename": "ReactDOM-profiling.js", "bundleType": "FB_WWW_PROFILING", "packageName": "react-dom", - "size": 345581, - "gzip": 63810 + "size": 346531, + "gzip": 64085 }, { "filename": "ReactNativeRenderer-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 257945, - "gzip": 45363 + "size": 258428, + "gzip": 45445 }, { "filename": "ReactFabric-profiling.js", "bundleType": "RN_FB_PROFILING", "packageName": "react-native-renderer", - "size": 250943, - "gzip": 44145 + "size": 250744, + "gzip": 44126 }, { "filename": "react.profiling.min.js", "bundleType": "UMD_PROFILING", "packageName": "react", - "size": 14818, - "gzip": 5369 + "size": 14552, + "gzip": 5255 }, { "filename": "react-dom.profiling.min.js", "bundleType": "UMD_PROFILING", "packageName": "react-dom", - "size": 110730, - "gzip": 35485 + "size": 110732, + "gzip": 35443 }, { "filename": "scheduler-tracing.development.js", "bundleType": "NODE_DEV", "packageName": "scheduler", - "size": 10878, - "gzip": 2594 + "size": 11062, + "gzip": 2681 }, { "filename": "scheduler-tracing.production.min.js", @@ -823,8 +823,8 @@ "filename": "SchedulerTracing-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "scheduler", - "size": 10267, - "gzip": 2151 + "size": 10470, + "gzip": 2260 }, { "filename": "SchedulerTracing-prod.js", @@ -886,29 +886,29 @@ "filename": "jest-react.development.js", "bundleType": "NODE_DEV", "packageName": "jest-react", - "size": 7455, - "gzip": 2737 + "size": 7100, + "gzip": 2546 }, { "filename": "jest-react.production.min.js", "bundleType": "NODE_PROD", "packageName": "jest-react", - "size": 2930, - "gzip": 1442 + "size": 2599, + "gzip": 1299 }, { "filename": "JestReact-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "jest-react", - "size": 4208, - "gzip": 1490 + "size": 5010, + "gzip": 1757 }, { "filename": "JestReact-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "jest-react", - "size": 3592, - "gzip": 1315 + "size": 3492, + "gzip": 1287 }, { "filename": "react-debug-tools.development.js", @@ -928,15 +928,15 @@ "filename": "eslint-plugin-react-hooks.development.js", "bundleType": "NODE_DEV", "packageName": "eslint-plugin-react-hooks", - "size": 75099, - "gzip": 17223 + "size": 77541, + "gzip": 17683 }, { "filename": "eslint-plugin-react-hooks.production.min.js", "bundleType": "NODE_PROD", "packageName": "eslint-plugin-react-hooks", - "size": 19731, - "gzip": 6811 + "size": 20485, + "gzip": 7082 }, { "filename": "ReactDOMFizzServer-dev.js", @@ -1026,71 +1026,71 @@ "filename": "ESLintPluginReactHooks-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "eslint-plugin-react-hooks", - "size": 80483, - "gzip": 17729 + "size": 83133, + "gzip": 18239 }, { "filename": "react-dom-unstable-fire.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 832471, - "gzip": 188731 + "size": 835944, + "gzip": 189154 }, { "filename": "react-dom-unstable-fire.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 107698, - "gzip": 34877 + "size": 107699, + "gzip": 34786 }, { "filename": "react-dom-unstable-fire.profiling.min.js", "bundleType": "UMD_PROFILING", "packageName": "react-dom", - "size": 110745, - "gzip": 35493 + "size": 110747, + "gzip": 35451 }, { "filename": "react-dom-unstable-fire.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 826725, - "gzip": 187104 + "size": 830198, + "gzip": 187505 }, { "filename": "react-dom-unstable-fire.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", "size": 107678, - "gzip": 34310 + "gzip": 34282 }, { "filename": "react-dom-unstable-fire.profiling.min.js", "bundleType": "NODE_PROFILING", "packageName": "react-dom", - "size": 110853, - "gzip": 34953 + "size": 110855, + "gzip": 34911 }, { "filename": "ReactFire-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 850863, - "gzip": 188551 + "size": 854519, + "gzip": 189023 }, { "filename": "ReactFire-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 327881, - "gzip": 60185 + "size": 328314, + "gzip": 60295 }, { "filename": "ReactFire-profiling.js", "bundleType": "FB_WWW_PROFILING", "packageName": "react-dom", - "size": 334366, - "gzip": 61586 + "size": 334864, + "gzip": 61709 }, { "filename": "jest-mock-scheduler.development.js", @@ -1152,29 +1152,253 @@ "filename": "react-events.development.js", "bundleType": "NODE_DEV", "packageName": "react-events", - "size": 1135, - "gzip": 623 + "size": 990, + "gzip": 545 }, { "filename": "react-events.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-events", - "size": 448, - "gzip": 328 + "size": 506, + "gzip": 343 }, { "filename": "ReactEvents-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-events", - "size": 1106, - "gzip": 613 + "size": 956, + "gzip": 536 }, { "filename": "ReactEvents-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-events", - "size": 643, - "gzip": 377 + "size": 687, + "gzip": 410 + }, + { + "filename": "react-events.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 1183, + "gzip": 605 + }, + { + "filename": "react-events.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 676, + "gzip": 420 + }, + { + "filename": "react-events-press.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 10325, + "gzip": 2630 + }, + { + "filename": "react-events-press.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 4058, + "gzip": 1507 + }, + { + "filename": "react-events-press.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-events", + "size": 10151, + "gzip": 2584 + }, + { + "filename": "react-events-press.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-events", + "size": 3892, + "gzip": 1451 + }, + { + "filename": "ReactEventsPress-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-events", + "size": 10480, + "gzip": 2636 + }, + { + "filename": "ReactEventsPress-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-events", + "size": 8000, + "gzip": 1905 + }, + { + "filename": "react-events-hover.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 5271, + "gzip": 1416 + }, + { + "filename": "react-events-hover.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 2312, + "gzip": 923 + }, + { + "filename": "react-events-hover.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-events", + "size": 5097, + "gzip": 1372 + }, + { + "filename": "react-events-hover.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-events", + "size": 2147, + "gzip": 865 + }, + { + "filename": "ReactEventsHover-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-events", + "size": 5113, + "gzip": 1386 + }, + { + "filename": "ReactEventsHover-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-events", + "size": 4279, + "gzip": 1130 + }, + { + "filename": "react-events-focus.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 3446, + "gzip": 1112 + }, + { + "filename": "react-events-focus.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 1563, + "gzip": 721 + }, + { + "filename": "react-events-focus.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-events", + "size": 3272, + "gzip": 1068 + }, + { + "filename": "react-events-focus.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-events", + "size": 1392, + "gzip": 659 + }, + { + "filename": "ReactEventsFocus-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-events", + "size": 3242, + "gzip": 1058 + }, + { + "filename": "ReactEventsFocus-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-events", + "size": 2552, + "gzip": 827 + }, + { + "filename": "react-events-swipe.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 8479, + "gzip": 2604 + }, + { + "filename": "react-events-swipe.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 3531, + "gzip": 1625 + }, + { + "filename": "react-events-swipe.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-events", + "size": 8305, + "gzip": 2571 + }, + { + "filename": "react-events-swipe.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-events", + "size": 3364, + "gzip": 1569 + }, + { + "filename": "ReactEventsSwipe-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-events", + "size": 6360, + "gzip": 1814 + }, + { + "filename": "ReactEventsSwipe-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-events", + "size": 6089, + "gzip": 1563 + }, + { + "filename": "react-events-drag.development.js", + "bundleType": "UMD_DEV", + "packageName": "react-events", + "size": 7733, + "gzip": 2450 + }, + { + "filename": "react-events-drag.production.min.js", + "bundleType": "UMD_PROD", + "packageName": "react-events", + "size": 3278, + "gzip": 1489 + }, + { + "filename": "react-events-drag.development.js", + "bundleType": "NODE_DEV", + "packageName": "react-events", + "size": 7560, + "gzip": 2415 + }, + { + "filename": "react-events-drag.production.min.js", + "bundleType": "NODE_PROD", + "packageName": "react-events", + "size": 3112, + "gzip": 1429 + }, + { + "filename": "ReactEventsDrag-dev.js", + "bundleType": "FB_WWW_DEV", + "packageName": "react-events", + "size": 5706, + "gzip": 1684 + }, + { + "filename": "ReactEventsDrag-prod.js", + "bundleType": "FB_WWW_PROD", + "packageName": "react-events", + "size": 5245, + "gzip": 1368 } ] } \ No newline at end of file