[Fiber] Double invoke Effects in StrictMode after Fast Refresh (#35962)
Sebastian "Sebbie" Silbermann committed
Apr 17, 2026 at 18:14 UTC
da9325b519376e2d65cdf6d509ade053e14ec5b3
2 files changed
+93
-1
packages/react-reconciler/src/ReactFiberBeginWork.js
+1
-1
@@ -3866,7 +3866,7 @@ function remountFiber(
3866
deletions.push(current);
3867
}
3868
3869
- newWorkInProgress.flags |= Placement;
3869
+ newWorkInProgress.flags |= Placement | PlacementDEV;
3870
3871
// Restart work from the new fiber.
3872
return newWorkInProgress;
packages/react-refresh/src/__tests__/ReactFresh-test.js
+92
@@ -2326,6 +2326,98 @@ describe('ReactFresh', () => {
2326
expect(finalEl.style.color).toBe('orange');
2327
}
2328
2329
+ it('double invokes effects after a forced remount in StrictMode', async () => {
2330
+ if (__DEV__) {
2331
+ const log = [];
2332
+
2333
+ const createAppV1 = () => {
2334
+ function Hello() {
2335
+ React.useEffect(() => {
2336
+ log.push('mount v1');
2337
+ return () => log.push('unmount v1');
2338
+ }, []);
2339
+ return <p style={{color: 'blue'}}>Hello</p>;
2340
+ }
2341
+ $RefreshReg$(Hello, 'Hello');
2342
+ $RefreshSig$(Hello, '1');
2343
+
2344
+ return Hello;
2345
+ };
2346
+
2347
+ const App = createAppV1();
2348
+
2349
+ await act(() => {
2350
+ root.render(
2351
+ <React.StrictMode>
2352
+ <App />
2353
+ </React.StrictMode>,
2354
+ );
2355
+ });
2356
+
2357
+ expect(log).toEqual(['mount v1', 'unmount v1', 'mount v1']);
2358
+ log.length = 0;
2359
+
2360
+ await patch(() => {
2361
+ function Hello() {
2362
+ React.useEffect(() => {
2363
+ log.push('mount v2');
2364
+ return () => log.push('unmount v2');
2365
+ }, []);
2366
+ return <p style={{color: 'red'}}>Hello</p>;
2367
+ }
2368
+ $RefreshReg$(Hello, 'Hello');
2369
+ $RefreshSig$(Hello, '2');
2370
+ return null;
2371
+ });
2372
+
2373
+ expect(container.firstChild.style.color).toBe('red');
2374
+ expect(log).toEqual(['unmount v1', 'mount v2', 'unmount v2', 'mount v2']);
2375
+ }
2376
+ });
2377
+
2378
+ it('double invokes an effect added during Fast Refresh remount in StrictMode', async () => {
2379
+ if (__DEV__) {
2380
+ const log = [];
2381
+
2382
+ const createAppV1 = () => {
2383
+ function Hello() {
2384
+ return <p style={{color: 'blue'}}>Hello</p>;
2385
+ }
2386
+ $RefreshReg$(Hello, 'Hello');
2387
+ $RefreshSig$(Hello, '1');
2388
+ return Hello;
2389
+ };
2390
+
2391
+ const App = createAppV1();
2392
+
2393
+ await act(() => {
2394
+ root.render(
2395
+ <React.StrictMode>
2396
+ <App />
2397
+ </React.StrictMode>,
2398
+ );
2399
+ });
2400
+
2401
+ expect(log).toEqual([]);
2402
+
2403
+ await patch(() => {
2404
+ function Hello() {
2405
+ React.useEffect(() => {
2406
+ log.push('mount v2');
2407
+ return () => log.push('unmount v2');
2408
+ }, []);
2409
+ return <p style={{color: 'red'}}>Hello</p>;
2410
+ }
2411
+ $RefreshReg$(Hello, 'Hello');
2412
+ $RefreshSig$(Hello, '2');
2413
+ return null;
2414
+ });
2415
+
2416
+ expect(container.firstChild.style.color).toBe('red');
2417
+ expect(log).toEqual(['mount v2', 'unmount v2', 'mount v2']);
2418
+ }
2419
+ });
2420
+
2421
it('resets hooks with dependencies on hot reload', async () => {
2422
if (__DEV__) {
2423
let useEffectWithEmptyArrayCalls = 0;