Skip to content

Commit 269c4e9

Browse files
authored
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
1 parent 8003ab9 commit 269c4e9

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ import {
124124
LayoutMask,
125125
PassiveMask,
126126
PlacementDEV,
127+
Visibility,
127128
} from './ReactFiberFlags';
128129
import {
129130
NoLanes,
@@ -3184,9 +3185,15 @@ function doubleInvokeEffectsInDEV(
31843185
) {
31853186
const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
31863187
const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
3188+
31873189
if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
31883190
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) {
31903197
disappearLayoutEffects(fiber);
31913198
disconnectPassiveEffect(fiber);
31923199
reappearLayoutEffects(root, fiber.alternate, fiber, false);

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ import {
124124
LayoutMask,
125125
PassiveMask,
126126
PlacementDEV,
127+
Visibility,
127128
} from './ReactFiberFlags';
128129
import {
129130
NoLanes,
@@ -3184,9 +3185,15 @@ function doubleInvokeEffectsInDEV(
31843185
) {
31853186
const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE;
31863187
const isInStrictMode = parentIsInStrictMode || isStrictModeFiber;
3188+
31873189
if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) {
31883190
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) {
31903197
disappearLayoutEffects(fiber);
31913198
disconnectPassiveEffect(fiber);
31923199
reappearLayoutEffects(root, fiber.alternate, fiber, false);

packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ describe('ReactOffscreenStrictMode', () => {
7171

7272
log = [];
7373

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+
7489
act(() => {
7590
ReactNoop.render(
7691
<React.StrictMode>
@@ -91,5 +106,51 @@ describe('ReactOffscreenStrictMode', () => {
91106
'A: useLayoutEffect mount',
92107
'A: useEffect mount',
93108
]);
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+
});
94155
});
95156
});

0 commit comments

Comments
 (0)