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();
问题根源与修复方案
核心问题
- 重复绑定事件:组件顶层直接绑定
beforeunload、storage事件,每次重渲染都会新增监听器,导致逻辑循环触发。 - DevTools触发storage事件:Chrome DevTools操作(如Storage面板刷新)会触发
storage事件,进而触发状态更新和页面跳转逻辑,引发重加载。 - 值类型不统一:
isSecondSreenOpen同时使用字符串和布尔值,判断逻辑易出错。 - 无事件清理:监听器未在组件卸载时移除,引发内存泄漏和异常触发。
修复步骤
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
相关产品推荐
相关产品推荐

