mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
* The exported '<React.StrictMode>' tag remains the same and opts legacy subtrees into strict mode level one ('mode == StrictModeL1'). This mode enables DEV-only double rendering, double component lifecycles, string ref warnings, legacy context warnings, etc. The primary purpose of this mode is to help detected render phase side effects. No new behavior. Roots created with experimental 'createRoot' and 'createBlockingRoot' APIs will also (for now) continue to default to strict mode level 1.
In a subsequent commit I will add support for a 'level' attribute on the '<React.StrictMode>' tag (as well as a new option supported by ). This will be the way to opt into strict mode level 2 ('mode == StrictModeL2'). This mode will enable DEV-only double invoking of effects on initial mount. This will simulate future Offscreen API semantics for trees being mounted, then hidden, and then shown again. The primary purpose of this mode is to enable applications to prepare for compatibility with the new Offscreen API (more information to follow shortly).
For now, this commit changes no public facing behavior. The only mechanism for opting into strict mode level 2 is the pre-existing 'enableDoubleInvokingEffects' feature flag (only enabled within Facebook for now).
* Renamed strict mode constants
StrictModeL1 -> StrictLegacyMode and StrictModeL2 -> StrictEffectsMode
* Renamed tests
* Split strict effects mode into two flags
One flag ('enableStrictEffects') enables strict mode level 2. It is similar to 'debugRenderPhaseSideEffectsForStrictMode' which enables srtict mode level 1.
The second flag ('createRootStrictEffectsByDefault') controls the default strict mode level for 'createRoot' trees. For now, all 'createRoot' trees remain level 1 by default. We will experiment with level 2 within Facebook.
This is a prerequisite for adding a configurable option to 'createRoot' that enables choosing a different StrictMode level than the default.
* Add StrictMode 'unstable_level' prop and createRoot 'unstable_strictModeLevel' option
New StrictMode 'unstable_level' prop allows specifying which level of strict mode to use. If no level attribute is specified, StrictLegacyMode will be used to maintain backwards compatibility. Otherwise the following is true:
* Level 0 does nothing
* Level 1 selects StrictLegacyMode
* Level 2 selects StrictEffectsMode (which includes StrictLegacyMode)
Levels can be increased with nesting (0 -> 1 -> 2) but not decreased.
This commit also adds a new 'unstable_strictModeLevel' option to the createRoot and createBatchedRoot APIs. This option can be used to override default behavior to increase or decrease the StrictMode level of the root.
A subsequent commit will add additional DEV warnings:
* If a nested StrictMode tag attempts to explicitly decrease the level
* If a level attribute changes in an update
636 lines
17 KiB
JavaScript
636 lines
17 KiB
JavaScript
/**
|
|
* 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(<App text={'mount'} />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
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(
|
|
<>
|
|
<ComponentWithEffects label={'one'} />
|
|
</>,
|
|
);
|
|
|
|
expect(Scheduler).toFlushUntilNextPaint([
|
|
'useLayoutEffect mount "one"',
|
|
'useLayoutEffect unmount "one"',
|
|
'useLayoutEffect mount "one"',
|
|
]);
|
|
});
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(
|
|
<>
|
|
<ComponentWithEffects label={'one'} />
|
|
<ComponentWithEffects label={'two'} />
|
|
</>,
|
|
);
|
|
|
|
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(
|
|
<>
|
|
<ComponentWithEffects label={'one'} />
|
|
</>,
|
|
);
|
|
|
|
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(
|
|
<>
|
|
<ComponentWithEffects label={'one'} />
|
|
<ComponentWithEffects label={'two'} />
|
|
</>,
|
|
);
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
'useLayoutEffect unmount',
|
|
'useEffect unmount',
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
]);
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App text={'update'} />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
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(<App text={'update'} />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
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(<App text={'update'} />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
]);
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App text={'update'} />);
|
|
});
|
|
|
|
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(<App />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'componentDidMount',
|
|
'componentWillUnmount',
|
|
'componentDidMount',
|
|
]);
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App text={'update'} />);
|
|
});
|
|
|
|
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(<App text={'mount'} />);
|
|
});
|
|
|
|
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 && <Child />;
|
|
}
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App />);
|
|
});
|
|
|
|
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 (
|
|
<>
|
|
<ClassChild text={text} />
|
|
<FunctionChild text={text} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App text={'mount'} />);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'componentDidMount',
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
'componentWillUnmount',
|
|
'useLayoutEffect unmount',
|
|
'useEffect unmount',
|
|
'componentDidMount',
|
|
'useLayoutEffect mount',
|
|
'useEffect mount',
|
|
]);
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(<App text={'mount'} />);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'useLayoutEffect unmount',
|
|
'useLayoutEffect mount',
|
|
'useEffect unmount',
|
|
'useEffect mount',
|
|
]);
|
|
|
|
ReactNoop.act(() => {
|
|
ReactNoop.render(null);
|
|
});
|
|
|
|
expect(Scheduler).toHaveYielded([
|
|
'componentWillUnmount',
|
|
'useLayoutEffect unmount',
|
|
'useEffect unmount',
|
|
]);
|
|
});
|
|
}
|
|
});
|