@samitouri / QOS-React / commits / da9325b519

[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;