You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chrome打开开发者工具致页面重载求助(React+Redux-Persist场景)

问题描述

打开Chrome开发者工具时,组件会重新渲染甚至从index.js重新加载,不确定是Chrome、React还是macOS导致的。

项目基于redux-persist开发,核心逻辑:

  • Page1渲染时自动打开Page2新标签页
  • 关闭Page2后,Page1的按钮恢复可用,可重新打开Page2
  • 关闭/刷新Page1时,Page2自动关闭并清除persist:root

环境版本

  • Chrome:Version 107.0.5304.110
  • React:18.2.0
  • macOS Monterey:12.3.1

代码示例

App.js

import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Page1 from "./Page1";
import Page2 from "./Page2";
const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Page1 />}></Route>
        <Route path="test" element={<Page2 />} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

Page1.js

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addItem, clearAll, createList, removeItem } from "./testReducer";
const Page1 = () => {
  const [isSecondScreenButtonDisable, setIsSecondScreenButtonDisable] = useState(false);
  const dispatch = useDispatch();
  const reducerList = useSelector((state) => state.counter);
  const array = [1];

  window.addEventListener("beforeunload", () => {
    localStorage.setItem("isSecondSreenOpen", "false");
    localStorage.removeItem("persist:root");
  });

  window.addEventListener("storage", () => {
    const isOpen = localStorage.getItem("isSecondSreenOpen");
    if (isOpen === "false") {
      setIsSecondScreenButtonDisable(false);
    } else {
      setIsSecondScreenButtonDisable(true);
    }
  });

  useEffect(() => {
    localStorage.setItem("isSecondSreenOpen", true);
    window.dispatchEvent(new Event("storage"));
    dispatch(createList(array));
  }, []);

  useEffect(() => {
    if (localStorage.getItem("isSecondSreenOpen") === "true") {
      window.open("test", "_blank");
    }
  }, []);

  return (
    <div className="App">
      <button
        disabled={isSecondScreenButtonDisable}
        onClick={() => {
          window.open("test", "_blank");
        }}
      >
        2. sayfa
      </button>
      <header className="App-header">
        <button onClick={() => dispatch(clearAll())}> Hepsini Sil</button>
        <button onClick={() => dispatch(addItem(100))}> Ekle</button>
        <button onClick={() => dispatch(removeItem())}> Sil</button>
        {reducerList.map((item, index) => {
          return (
            <div key={item + index}>
              {index}-{item}
            </div>
          );
        })}
      </header>
    </div>
  );
};

export default Page1;

Page2.js

import { useEffect } from "react";
import { useSelector } from "react-redux";
const Page2 = () => {
  const reducerList = useSelector((state) => state.counter);

  const onCloseEvent = () => {
    localStorage.setItem("isSecondSreenOpen", false);
    window.close();
  };

  window.addEventListener("beforeunload", () => {
    localStorage.setItem("isSecondSreenOpen", false);
  });

  useEffect(() => {
    localStorage.setItem("isSecondSreenOpen", true);
    window.addEventListener("storage", () => {
      const isOpen = localStorage.getItem("isSecondSreenOpen");
      if (isOpen === "false") {
        onCloseEvent();
      }
    });
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        {reducerList.map((item, index) => {
          return (
            <div key={item}>
              {index}-{item}
            </div>
          );
        })}
      </header>
    </div>
  );
};

export default Page2;

store.js

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import {
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist";
import persistStore from "redux-persist/es/persistStore";
import storage from "redux-persist/lib/storage";
import { createStateSyncMiddleware } from "redux-state-sync";
// import  secondReducer  from "./secondReducer";
import testReducer from "./testReducer";

const persistConfig = {
  key: "root",
  version: 1,
  storage,
  // blacklist: ['secondReducer']
};

const rootReducer = combineReducers({
  counter: testReducer,
  // secondReducer: secondReducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);


export const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
      },
    }).concat(
      createStateSyncMiddleware({
        blacklist: [PERSIST, REHYDRATE],
      })
    ),
});

export  const persistor = persistStore(store);

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { PersistGate } from "redux-persist/lib/integration/react";
import { initMessageListener } from "redux-state-sync";

const root = ReactDOM.createRoot(document.getElementById("root"));
initMessageListener(store);
root.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

reportWebVitals();

问题根源与修复方案

核心问题

  1. 重复绑定事件:组件顶层直接绑定beforeunload、storage事件,每次重渲染都会新增监听器,导致逻辑循环触发。
  2. DevTools触发storage事件:Chrome DevTools操作(如Storage面板刷新)会触发storage事件,进而触发状态更新和页面跳转逻辑,引发重加载。
  3. 值类型不统一:isSecondSreenOpen同时使用字符串和布尔值,判断逻辑易出错。
  4. 无事件清理:监听器未在组件卸载时移除,引发内存泄漏和异常触发。

修复步骤

1. 规范事件绑定与清理

在useEffect中绑定事件,并返回清理函数移除监听器,避免重复绑定:
修改Page1.js的事件逻辑:

useEffect(() => {
  const handleBeforeUnload = () => {
    localStorage.setItem("isSecondSreenOpen", "false");
    localStorage.removeItem("persist:root");
  };

  const handleStorage = () => {
    const isOpen = localStorage.getItem("isSecondSreenOpen");
    setIsSecondScreenButtonDisable(isOpen === "true");
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  window.addEventListener("storage", handleStorage);

  return () => {
    window.removeEventListener("beforeunload", handleBeforeUnload);
    window.removeEventListener("storage", handleStorage);
  };
}, []);

Page2.js同理修改:

useEffect(() => {
  const handleBeforeUnload = () => {
    localStorage.setItem("isSecondSreenOpen", "false");
  };

  const handleStorage = () => {
    const isOpen = localStorage.getItem("isSecondSreenOpen");
    if (isOpen === "false") {
      onCloseEvent();
    }
  };

  window.addEventListener("beforeunload", handleBeforeUnload);
  window.addEventListener("storage", handleStorage);

  return () => {
    window.removeEventListener("beforeunload", handleBeforeUnload);
    window.removeEventListener("storage", handleStorage);
  };
}, []);

2. 限制新标签页触发逻辑

避免DevTools触发重复打开标签页,用sessionStorage标记是否已打开:

useEffect(() => {
  const hasOpened = sessionStorage.getItem("page2Opened");
  if (!hasOpened && localStorage.getItem("isSecondSreenOpen") === "true") {
    window.open("test", "_blank");
    sessionStorage.setItem("page2Opened", "true");
  }
}, []);

3. 统一localStorage值类型

所有isSecondSreenOpen的设置都用字符串类型,避免判断错误:

// 统一用字符串
localStorage.setItem("isSecondSreenOpen", "false");
// 不要用布尔值 localStorage.setItem("isSecondSreenOpen", false);

4. 优化Redux状态同步

开发环境可临时禁用redux-state-sync中间件,排查是否是状态同步引发的重渲染;生产环境确保中间件配置只同步必要状态。

内容的提问来源于stack exchange,提问作者Neo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 07:20:28