Skip to content

Commit

Permalink
Add StrictMode level prop and createRoot unstable_strictModeLevel opt…
Browse files Browse the repository at this point in the history
…ion (#20849)

* 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
  • Loading branch information
Brian Vaughn authored Feb 24, 2021
1 parent 4190a34 commit 9209c30
Show file tree
Hide file tree
Showing 43 changed files with 1,243 additions and 934 deletions.
14 changes: 13 additions & 1 deletion packages/react-dom/src/client/ReactDOMRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type RootOptions = {
mutableSources?: Array<MutableSource<any>>,
...
},
unstable_strictModeLevel?: number,
...
};

Expand Down Expand Up @@ -128,7 +129,18 @@ function createRootImpl(
options.hydrationOptions != null &&
options.hydrationOptions.mutableSources) ||
null;
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
const strictModeLevelOverride =
options != null && options.unstable_strictModeLevel != null
? options.unstable_strictModeLevel
: null;

const root = createContainer(
container,
tag,
hydrate,
hydrationCallbacks,
strictModeLevelOverride,
);
markContainerAsRoot(root.current, container);

const rootContainerElement =
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-renderer/src/ReactFabric.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ function render(
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(containerTag, LegacyRoot, false, null);
root = createContainer(containerTag, LegacyRoot, false, null, null);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-renderer/src/ReactNativeRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ function render(
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = createContainer(containerTag, LegacyRoot, false, null);
root = createContainer(containerTag, LegacyRoot, false, null, null);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
Expand Down
5 changes: 4 additions & 1 deletion packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (!root) {
const container = {rootID: rootID, pendingChildren: [], children: []};
rootContainers.set(rootID, container);
root = NoopRenderer.createContainer(container, tag, false, null);
root = NoopRenderer.createContainer(container, tag, false, null, null);
roots.set(rootID, root);
}
return root.current.stateNode.containerInfo;
Expand All @@ -740,6 +740,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
ConcurrentRoot,
false,
null,
null,
);
return {
_Scheduler: Scheduler,
Expand All @@ -766,6 +767,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
BlockingRoot,
false,
null,
null,
);
return {
_Scheduler: Scheduler,
Expand All @@ -792,6 +794,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
LegacyRoot,
false,
null,
null,
);
return {
_Scheduler: Scheduler,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactChildFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from './ReactFiber.new';
import {emptyRefsObject} from './ReactFiberClassComponent.new';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.new';
import {StrictMode} from './ReactTypeOfMode';
import {StrictLegacyMode} from './ReactTypeOfMode';

let didWarnAboutMaps;
let didWarnAboutGenerators;
Expand Down Expand Up @@ -114,7 +114,7 @@ function coerceRef(
// TODO: Clean this up once we turn on the string ref warning for
// everyone, because the strict mode case will no longer be relevant
if (
(returnFiber.mode & StrictMode || warnAboutStringRefs) &&
(returnFiber.mode & StrictLegacyMode || warnAboutStringRefs) &&
// We warn in ReactElement.js if owner and self are equal for string refs
// because these cannot be automatically converted to an arrow function
// using a codemod. Therefore, we don't have to warn about string refs again.
Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/ReactChildFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
} from './ReactFiber.old';
import {emptyRefsObject} from './ReactFiberClassComponent.old';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading.old';
import {StrictMode} from './ReactTypeOfMode';
import {StrictLegacyMode} from './ReactTypeOfMode';

let didWarnAboutMaps;
let didWarnAboutGenerators;
Expand Down Expand Up @@ -114,7 +114,7 @@ function coerceRef(
// TODO: Clean this up once we turn on the string ref warning for
// everyone, because the strict mode case will no longer be relevant
if (
(returnFiber.mode & StrictMode || warnAboutStringRefs) &&
(returnFiber.mode & StrictLegacyMode || warnAboutStringRefs) &&
// We warn in ReactElement.js if owner and self are equal for string refs
// because these cannot be automatically converted to an arrow function
// using a codemod. Therefore, we don't have to warn about string refs again.
Expand Down
64 changes: 58 additions & 6 deletions packages/react-reconciler/src/ReactFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent';

import invariant from 'shared/invariant';
import {
createRootStrictEffectsByDefault,
enableCache,
enableStrictEffects,
enableProfilerTimer,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
Expand Down Expand Up @@ -64,7 +66,8 @@ import {
ConcurrentMode,
DebugTracingMode,
ProfileMode,
StrictMode,
StrictLegacyMode,
StrictEffectsMode,
BlockingMode,
} from './ReactTypeOfMode';
import {
Expand Down Expand Up @@ -418,12 +421,47 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
return workInProgress;
}

export function createHostRootFiber(tag: RootTag): Fiber {
export function createHostRootFiber(
tag: RootTag,
strictModeLevelOverride: null | number,
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
mode = ConcurrentMode | BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (strictModeLevelOverride >= 2) {
mode |= StrictEffectsMode;
}
}
} else {
if (enableStrictEffects && createRootStrictEffectsByDefault) {
mode |= StrictLegacyMode | StrictEffectsMode;
} else {
mode |= StrictLegacyMode;
}
}
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
mode = BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (strictModeLevelOverride >= 2) {
mode |= StrictEffectsMode;
}
}
} else {
if (enableStrictEffects && createRootStrictEffectsByDefault) {
mode |= StrictLegacyMode | StrictEffectsMode;
} else {
mode |= StrictLegacyMode;
}
}
} else {
mode = NoMode;
}
Expand Down Expand Up @@ -472,7 +510,21 @@ export function createFiberFromTypeAndProps(
break;
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode;
mode |= StrictMode;

// Legacy strict mode (<StrictMode> without any level prop) defaults to level 1.
const level =
pendingProps.unstable_level == null ? 1 : pendingProps.unstable_level;

// Levels cascade; higher levels inherit all lower level modes.
// It is explicitly not supported to lower a mode with nesting, only to increase it.
if (level >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (level >= 2) {
mode |= StrictEffectsMode;
}
}
break;
case REACT_PROFILER_TYPE:
return createFiberFromProfiler(pendingProps, mode, lanes, key);
Expand Down
64 changes: 58 additions & 6 deletions packages/react-reconciler/src/ReactFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import type {OffscreenProps} from './ReactFiberOffscreenComponent';

import invariant from 'shared/invariant';
import {
createRootStrictEffectsByDefault,
enableCache,
enableStrictEffects,
enableProfilerTimer,
enableScopeAPI,
enableCache,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
Expand Down Expand Up @@ -64,7 +66,8 @@ import {
ConcurrentMode,
DebugTracingMode,
ProfileMode,
StrictMode,
StrictLegacyMode,
StrictEffectsMode,
BlockingMode,
} from './ReactTypeOfMode';
import {
Expand Down Expand Up @@ -418,12 +421,47 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
return workInProgress;
}

export function createHostRootFiber(tag: RootTag): Fiber {
export function createHostRootFiber(
tag: RootTag,
strictModeLevelOverride: null | number,
): Fiber {
let mode;
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode;
mode = ConcurrentMode | BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (strictModeLevelOverride >= 2) {
mode |= StrictEffectsMode;
}
}
} else {
if (enableStrictEffects && createRootStrictEffectsByDefault) {
mode |= StrictLegacyMode | StrictEffectsMode;
} else {
mode |= StrictLegacyMode;
}
}
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode;
mode = BlockingMode;
if (strictModeLevelOverride !== null) {
if (strictModeLevelOverride >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (strictModeLevelOverride >= 2) {
mode |= StrictEffectsMode;
}
}
} else {
if (enableStrictEffects && createRootStrictEffectsByDefault) {
mode |= StrictLegacyMode | StrictEffectsMode;
} else {
mode |= StrictLegacyMode;
}
}
} else {
mode = NoMode;
}
Expand Down Expand Up @@ -472,7 +510,21 @@ export function createFiberFromTypeAndProps(
break;
case REACT_STRICT_MODE_TYPE:
fiberTag = Mode;
mode |= StrictMode;

// Legacy strict mode (<StrictMode> without any level prop) defaults to level 1.
const level =
pendingProps.unstable_level == null ? 1 : pendingProps.unstable_level;

// Levels cascade; higher levels inherit all lower level modes.
// It is explicitly not supported to lower a mode with nesting, only to increase it.
if (level >= 1) {
mode |= StrictLegacyMode;
}
if (enableStrictEffects) {
if (level >= 2) {
mode |= StrictEffectsMode;
}
}
break;
case REACT_PROFILER_TYPE:
return createFiberFromProfiler(pendingProps, mode, lanes, key);
Expand Down
12 changes: 6 additions & 6 deletions packages/react-reconciler/src/ReactFiberBeginWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ import {
ConcurrentMode,
NoMode,
ProfileMode,
StrictMode,
StrictLegacyMode,
BlockingMode,
} from './ReactTypeOfMode';
import {
Expand Down Expand Up @@ -357,7 +357,7 @@ function updateForwardRef(
);
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
workInProgress.mode & StrictLegacyMode
) {
disableLogs();
try {
Expand Down Expand Up @@ -889,7 +889,7 @@ function updateFunctionComponent(
);
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
workInProgress.mode & StrictLegacyMode
) {
disableLogs();
try {
Expand Down Expand Up @@ -1068,7 +1068,7 @@ function finishClassComponent(
nextChildren = instance.render();
if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
workInProgress.mode & StrictLegacyMode
) {
disableLogs();
try {
Expand Down Expand Up @@ -1478,7 +1478,7 @@ function mountIndeterminateComponent(
}
}

if (workInProgress.mode & StrictMode) {
if (workInProgress.mode & StrictLegacyMode) {
ReactStrictModeWarnings.recordLegacyContextWarning(workInProgress, null);
}

Expand Down Expand Up @@ -1615,7 +1615,7 @@ function mountIndeterminateComponent(

if (
debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode
workInProgress.mode & StrictLegacyMode
) {
disableLogs();
try {
Expand Down
Loading

0 comments on commit 9209c30

Please sign in to comment.