Prevent infinite re-renders in StrictMode + Offscreen (#25203)
* Prevent infinite re-render in StrictMode + Offscreen * Only fire effects for Offscreen when it is revealed * Move setting debug fiber into if branch * Move settings of debug fiber out of if branch
Samuel Susla committed
Sep 9, 2022 at 21:10 UTC
269c4e975f10838ab36d08763f3ddf635f24690a
3 files changed
+77
-2
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+8
-1
@@ -124,6 +124,7 @@ import {
124
LayoutMask,
125
PassiveMask,
126
PlacementDEV,
127
+ Visibility,
128
} from './ReactFiberFlags';
129
import {
130
NoLanes,
@@ -3184,9 +3185,15 @@ function doubleInvokeEffectsInDEV(
3185
) {
3186
const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
3187
const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
3188
+
3189
if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
3190
setCurrentDebugFiberInDEV(fiber);
3189
- if (isInStrictMode) {
3191
+ const isNotOffscreen = fiber.tag !== OffscreenComponent;
3192
+ // Checks if Offscreen is being revealed. For all other components, evaluates to true.
3193
+ const hasOffscreenBecomeVisible =
3194
+ isNotOffscreen ||
3195
+ (fiber.flags & Visibility && fiber.memoizedState === null);
3196
+ if (isInStrictMode && hasOffscreenBecomeVisible) {
3197
disappearLayoutEffects(fiber);
3198
disconnectPassiveEffect(fiber);
3199
reappearLayoutEffects(root, fiber.alternate, fiber, false);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+8
-1
@@ -124,6 +124,7 @@ import {
124
LayoutMask,
125
PassiveMask,
126
PlacementDEV,
127
+ Visibility,
128
} from './ReactFiberFlags';
129
import {
130
NoLanes,
@@ -3184,9 +3185,15 @@ function doubleInvokeEffectsInDEV(
3185
) {
3186
const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
3187
const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
3188
+
3189
if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
3190
setCurrentDebugFiberInDEV(fiber);
3189
- if (isInStrictMode) {
3191
+ const isNotOffscreen = fiber.tag !== OffscreenComponent;
3192
+ // Checks if Offscreen is being revealed. For all other components, evaluates to true.
3193
+ const hasOffscreenBecomeVisible =
3194
+ isNotOffscreen ||
3195
+ (fiber.flags & Visibility && fiber.memoizedState === null);
3196
+ if (isInStrictMode && hasOffscreenBecomeVisible) {
3197
disappearLayoutEffects(fiber);
3198
disconnectPassiveEffect(fiber);
3199
reappearLayoutEffects(root, fiber.alternate, fiber, false);
packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js
+61
@@ -71,6 +71,21 @@ describe('ReactOffscreenStrictMode', () => {
71
72
log = [];
73
74
+ act(() => {
75
+ ReactNoop.render(
76
+ <React.StrictMode>
77
+ <Offscreen mode="hidden">
78
+ <Component label="A" />
79
+ <Component label="B" />
80
+ </Offscreen>
81
+ </React.StrictMode>,
82
+ );
83
+ });
84
+
85
+ expect(log).toEqual(['A: render', 'A: render', 'B: render', 'B: render']);
86
+
87
+ log = [];
88
+
89
act(() => {
90
ReactNoop.render(
91
<React.StrictMode>
@@ -91,5 +106,51 @@ describe('ReactOffscreenStrictMode', () => {
106
'A: useLayoutEffect mount',
107
'A: useEffect mount',
108
]);
109
+
110
+ log = [];
111
+
112
+ act(() => {
113
+ ReactNoop.render(
114
+ <React.StrictMode>
115
+ <Offscreen mode="hidden">
116
+ <Component label="A" />
117
+ </Offscreen>
118
+ </React.StrictMode>,
119
+ );
120
+ });
121
+
122
+ expect(log).toEqual([
123
+ 'A: useLayoutEffect unmount',
124
+ 'A: useEffect unmount',
125
+ 'A: render',
126
+ 'A: render',
127
+ ]);
128
+ });
129
+
130
+ it('should not cause infinite render loop when StrictMode is used with Suspense and synchronous set states', () => {
131
+ // This is a regression test, see https://github.com/facebook/react/pull/25179 for more details.
132
+ function App() {
133
+ const [state, setState] = React.useState(false);
134
+
135
+ React.useLayoutEffect(() => {
136
+ setState(true);
137
+ }, []);
138
+
139
+ React.useEffect(() => {
140
+ // Empty useEffect with empty dependency array is needed to trigger infinite render loop.
141
+ }, []);
142
+
143
+ return state;
144
+ }
145
+
146
+ act(() => {
147
+ ReactNoop.render(
148
+ <React.StrictMode>
149
+ <React.Suspense>
150
+ <App />
151
+ </React.Suspense>
152
+ </React.StrictMode>,
153
+ );
154
+ });
155
});
156
});