/** * 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 */ 'use strict'; let React; let ReactNoop; let Scheduler; describe('StrictEffectsMode defaults', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactNoop = require('react-noop-renderer'); Scheduler = require('scheduler'); const ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableStrictEffects = __DEV__; ReactFeatureFlags.createRootStrictEffectsByDefault = __DEV__; }); it('should not double invoke effects in legacy mode', () => { function App({text}) { React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect mount'); return () => Scheduler.unstable_yieldValue('useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); }); return text; } ReactNoop.act(() => { ReactNoop.renderLegacySyncRoot(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', ]); }); it('should not double invoke class lifecycles in legacy mode', () => { class App extends React.PureComponent { componentDidMount() { Scheduler.unstable_yieldValue('componentDidMount'); } componentDidUpdate() { Scheduler.unstable_yieldValue('componentDidUpdate'); } componentWillUnmount() { Scheduler.unstable_yieldValue('componentWillUnmount'); } render() { return this.props.text; } } ReactNoop.act(() => { ReactNoop.renderLegacySyncRoot(); }); expect(Scheduler).toHaveYielded(['componentDidMount']); }); if (__DEV__) { it('should flush double-invoked effects within the same frame as layout effects if there are no passive effects', () => { function ComponentWithEffects({label}) { React.useLayoutEffect(() => { Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); return () => Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); }); return label; } ReactNoop.act(() => { ReactNoop.render( <> , ); expect(Scheduler).toFlushUntilNextPaint([ 'useLayoutEffect mount "one"', 'useLayoutEffect unmount "one"', 'useLayoutEffect mount "one"', ]); }); ReactNoop.act(() => { ReactNoop.render( <> , ); expect(Scheduler).toFlushUntilNextPaint([ // Cleanup and re-run "one" (and "two") since there is no dependencies array. 'useLayoutEffect unmount "one"', 'useLayoutEffect mount "one"', 'useLayoutEffect mount "two"', // Since "two" is new, it should be double-invoked. 'useLayoutEffect unmount "two"', 'useLayoutEffect mount "two"', ]); }); }); // This test also verifies that double-invoked effects flush synchronously // within the same frame as passive effects. it('should double invoke effects only for newly mounted components', () => { function ComponentWithEffects({label}) { React.useEffect(() => { Scheduler.unstable_yieldValue(`useEffect mount "${label}"`); return () => Scheduler.unstable_yieldValue(`useEffect unmount "${label}"`); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue(`useLayoutEffect mount "${label}"`); return () => Scheduler.unstable_yieldValue(`useLayoutEffect unmount "${label}"`); }); return label; } ReactNoop.act(() => { ReactNoop.render( <> , ); expect(Scheduler).toFlushAndYieldThrough([ 'useLayoutEffect mount "one"', ]); expect(Scheduler).toFlushAndYield([ 'useEffect mount "one"', 'useLayoutEffect unmount "one"', 'useEffect unmount "one"', 'useLayoutEffect mount "one"', 'useEffect mount "one"', ]); }); ReactNoop.act(() => { ReactNoop.render( <> , ); expect(Scheduler).toFlushAndYieldThrough([ // Cleanup and re-run "one" (and "two") since there is no dependencies array. 'useLayoutEffect unmount "one"', 'useLayoutEffect mount "one"', 'useLayoutEffect mount "two"', ]); expect(Scheduler).toFlushAndYield([ 'useEffect unmount "one"', 'useEffect mount "one"', 'useEffect mount "two"', // Since "two" is new, it should be double-invoked. 'useLayoutEffect unmount "two"', 'useEffect unmount "two"', 'useLayoutEffect mount "two"', 'useEffect mount "two"', ]); }); }); it('double invoking for effects for modern roots', () => { function App({text}) { React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect mount'); return () => Scheduler.unstable_yieldValue('useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); }); return text; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', 'useLayoutEffect unmount', 'useEffect unmount', 'useLayoutEffect mount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect unmount', 'useLayoutEffect mount', 'useEffect unmount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect unmount', 'useEffect unmount', ]); }); it('multiple effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { function App({text}) { React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect One mount'); return () => Scheduler.unstable_yieldValue('useEffect One unmount'); }); React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect Two mount'); return () => Scheduler.unstable_yieldValue('useEffect Two unmount'); }); return text; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useEffect One mount', 'useEffect Two mount', 'useEffect One unmount', 'useEffect Two unmount', 'useEffect One mount', 'useEffect Two mount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useEffect One unmount', 'useEffect Two unmount', 'useEffect One mount', 'useEffect Two mount', ]); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded([ 'useEffect One unmount', 'useEffect Two unmount', ]); }); it('multiple layout effects are double invoked in the right order (all mounted, all unmounted, all remounted)', () => { function App({text}) { React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect One mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect One unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect Two mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect Two unmount'); }); return text; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect One mount', 'useLayoutEffect Two mount', 'useLayoutEffect One unmount', 'useLayoutEffect Two unmount', 'useLayoutEffect One mount', 'useLayoutEffect Two mount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect One unmount', 'useLayoutEffect Two unmount', 'useLayoutEffect One mount', 'useLayoutEffect Two mount', ]); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect One unmount', 'useLayoutEffect Two unmount', ]); }); it('useEffect and useLayoutEffect is called twice when there is no unmount', () => { function App({text}) { React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect mount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect mount'); }); return text; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', 'useLayoutEffect mount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect mount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded([]); }); it('passes the right context to class component lifecycles', () => { class App extends React.PureComponent { test() {} componentDidMount() { this.test(); Scheduler.unstable_yieldValue('componentDidMount'); } componentDidUpdate() { this.test(); Scheduler.unstable_yieldValue('componentDidUpdate'); } componentWillUnmount() { this.test(); Scheduler.unstable_yieldValue('componentWillUnmount'); } render() { return null; } } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'componentDidMount', 'componentWillUnmount', 'componentDidMount', ]); }); it('double invoking works for class components', () => { class App extends React.PureComponent { componentDidMount() { Scheduler.unstable_yieldValue('componentDidMount'); } componentDidUpdate() { Scheduler.unstable_yieldValue('componentDidUpdate'); } componentWillUnmount() { Scheduler.unstable_yieldValue('componentWillUnmount'); } render() { return this.props.text; } } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'componentDidMount', 'componentWillUnmount', 'componentDidMount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded(['componentDidUpdate']); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded(['componentWillUnmount']); }); it('double flushing passive effects only results in one double invoke', () => { function App({text}) { const [state, setState] = React.useState(0); React.useEffect(() => { if (state !== 1) { setState(1); } Scheduler.unstable_yieldValue('useEffect mount'); return () => Scheduler.unstable_yieldValue('useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); }); Scheduler.unstable_yieldValue(text); return text; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'mount', 'useLayoutEffect mount', 'useEffect mount', 'useLayoutEffect unmount', 'useEffect unmount', 'useLayoutEffect mount', 'useEffect mount', 'mount', 'useLayoutEffect unmount', 'useLayoutEffect mount', 'useEffect unmount', 'useEffect mount', ]); }); it('newly mounted components after initial mount get double invoked', () => { let _setShowChild; function Child() { React.useEffect(() => { Scheduler.unstable_yieldValue('Child useEffect mount'); return () => Scheduler.unstable_yieldValue('Child useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('Child useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('Child useLayoutEffect unmount'); }); return null; } function App() { const [showChild, setShowChild] = React.useState(false); _setShowChild = setShowChild; React.useEffect(() => { Scheduler.unstable_yieldValue('App useEffect mount'); return () => Scheduler.unstable_yieldValue('App useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('App useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('App useLayoutEffect unmount'); }); return showChild && ; } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'App useLayoutEffect mount', 'App useEffect mount', 'App useLayoutEffect unmount', 'App useEffect unmount', 'App useLayoutEffect mount', 'App useEffect mount', ]); ReactNoop.act(() => { _setShowChild(true); }); expect(Scheduler).toHaveYielded([ 'App useLayoutEffect unmount', 'Child useLayoutEffect mount', 'App useLayoutEffect mount', 'App useEffect unmount', 'Child useEffect mount', 'App useEffect mount', 'Child useLayoutEffect unmount', 'Child useEffect unmount', 'Child useLayoutEffect mount', 'Child useEffect mount', ]); }); it('classes and functions are double invoked together correctly', () => { class ClassChild extends React.PureComponent { componentDidMount() { Scheduler.unstable_yieldValue('componentDidMount'); } componentWillUnmount() { Scheduler.unstable_yieldValue('componentWillUnmount'); } render() { return this.props.text; } } function FunctionChild({text}) { React.useEffect(() => { Scheduler.unstable_yieldValue('useEffect mount'); return () => Scheduler.unstable_yieldValue('useEffect unmount'); }); React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('useLayoutEffect mount'); return () => Scheduler.unstable_yieldValue('useLayoutEffect unmount'); }); return text; } function App({text}) { return ( <> ); } ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'componentDidMount', 'useLayoutEffect mount', 'useEffect mount', 'componentWillUnmount', 'useLayoutEffect unmount', 'useEffect unmount', 'componentDidMount', 'useLayoutEffect mount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(); }); expect(Scheduler).toHaveYielded([ 'useLayoutEffect unmount', 'useLayoutEffect mount', 'useEffect unmount', 'useEffect mount', ]); ReactNoop.act(() => { ReactNoop.render(null); }); expect(Scheduler).toHaveYielded([ 'componentWillUnmount', 'useLayoutEffect unmount', 'useEffect unmount', ]); }); } });