@samitouri / QOS-React-2 / commits / 269c4e975f

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 });