非StrictMode下React useEffect执行两次问题求助
Socket应用中useEffect执行两次的问题排查与解决
问题描述
我正在开发一个基于Socket的应用,遇到了useEffect执行两次的问题,这引发了竞态、重复emit和重复注册的情况。将包含emit逻辑的页面简化为测试页后,问题依然存在。检查代码发现:
index.tsx未启用StrictModeWrapper组件内部嵌套了React.StrictMode- 创建房间跳转到测试页后,日志显示
TestContainer的useEffect执行了两次
相关代码
index.tsx(无StrictMode)
const container = document.getElementById("root")!; const root = createRoot(container); root.render( <Provider store={store}> <App /> </Provider> );
src/App.tsx
export function App() { return ( <Wrapper data-test="app"> <Route path="/" element={<HomePage />} /> <Route path="/test" element={<TestPage />} /> <Route path="/join-room" element={<JoinRoomPage />} /> <Route path="/:id" element={<RoomPage />} /> <Route path="/admin-add-answer" element={<AdminAddAnswerPage />} /> <Route path="/admin-add-question" element={<AdminAddQuestionPage />} /> </Wrapper> ); }
src/wrapper
export const Wrapper: React.FunctionComponent<WrapperProps> = ( props: WrapperProps ): React.ReactElement<WrapperProps> => { const [user, setUser] = React.useState<User>(); const [room, setRoom] = React.useState<Room>(); const [round, setRound] = React.useState<Round>(); return ( <React.StrictMode> <BrowserRouter data-test="wrapper-router"> <Provider store={store} data-test="wrapper-store-provider"> <SocketProvider> <UserContext.Provider value={{ user, setUser }}> <RoomContext.Provider value={{ room, setRoom }}> <RoundContext.Provider value={{ round, setRound }}> <Box sx={{ fontFamily: "main", height: "100vh", width: "100vw", position: "relative", }} > <Routes data-test="wrapper-routes">{props.children}</Routes> </Box> </RoundContext.Provider> </RoomContext.Provider> </UserContext.Provider> </SocketProvider> </Provider> </BrowserRouter> </React.StrictMode> ); };
src/pages/home
export const HomeContainer = () => { const navigate = useNavigate(); const [createRoom, { isSuccess, data }] = useCreateRoomMutation(); const socket = useContext(SocketContext); const { user, setUser } = useContext(UserContext); const [settings, setSettings] = useState({ points_to_win: 10, max_time_per_round: 15, }); useEffect(() => { if (isSuccess && user) { navigate(`/test`); } }, [isSuccess, data, navigate, user]); const handleSubmit = (settings: { points_to_win: number; max_time_per_round: number; }) => { try { const newUser = { _id: uuidv4(), is_tv: true, nickname: "main_projector", }; sessionStorage.setItem("user", JSON.stringify(newUser)); setUser(newUser); createRoom({ projector_id: socket.id, settings }); } catch (e) { console.log("Failed to create a new room", e); } }; return ( <HomePresentational handleSubmit={handleSubmit} onClickJoin={() => navigate("/join-room")} settings={settings} setSettings={(s: { points_to_win: number; max_time_per_round: number }) => setSettings(s) } /> ); };
src/pages/test
export const TestContainer = () => { console.log("test container"); useEffect(() => { console.log("test container useEffect"); }, []); return <Box>This is test container</Box>; };
日志输出
test-container.tsx:5 test container test-container.tsx:8 test container useEffect test-container.tsx:8 test container useEffect
问题根源
React的StrictMode在开发环境下会刻意重复执行以下操作:
- 组件的渲染函数
useState、useMemo、useReducer的初始化函数useEffect和useLayoutEffect的副作用函数
这么做是为了检测潜在bug,比如未清理的副作用、重复订阅、非幂等的初始化逻辑等。你的Wrapper组件中嵌套了React.StrictMode,这就是useEffect执行两次的直接原因。
解决办法
1. 临时禁用StrictMode(开发调试用)
直接移除Wrapper组件中的React.StrictMode包裹,开发环境下就不会重复执行副作用:
export const Wrapper: React.FunctionComponent<WrapperProps> = ( props: WrapperProps ): React.ReactElement<WrapperProps> => { const [user, setUser] = React.useState<User>(); const [room, setRoom] = React.useState<Room>(); const [round, setRound] = React.useState<Round>(); return ( <BrowserRouter data-test="wrapper-router"> <Provider store={store} data-test="wrapper-store-provider"> <SocketProvider> <UserContext.Provider value={{ user, setUser }}> <RoomContext.Provider value={{ room, setRoom }}> <RoundContext.Provider value={{ round, setRound }}> <Box sx={{ fontFamily: "main", height: "100vh", width: "100vw", position: "relative", }} > <Routes data-test="wrapper-routes">{props.children}</Routes> </Box> </RoundContext.Provider> </RoomContext.Provider> </UserContext.Provider> </SocketProvider> </Provider> </BrowserRouter> ); };
2. 正确处理副作用(推荐生产环境做法)
StrictMode的重复执行是在提醒你:副作用代码应该是幂等的,并且要有正确的清理逻辑。对于Socket操作,务必在useEffect的清理函数中取消监听、断开不必要的连接:
useEffect(() => { // 执行Socket emit或注册监听 socket.emit('join-room', roomId); const messageHandler = (message) => { // 处理消息逻辑 }; socket.on('room-message', messageHandler); // 清理函数:组件卸载或依赖变化时自动执行 return () => { // 移除监听,避免重复注册 socket.off('room-message', messageHandler); // 按需断开连接或执行其他清理操作 }; }, [socket, roomId]);
这样即使StrictMode重复执行副作用,清理函数也会先执行一次,避免重复注册和内存泄漏。
3. 区分开发/生产环境
如果想在开发环境保留StrictMode的检测能力,同时避免重复执行带来的问题,可以通过环境变量控制:
export const Wrapper: React.FunctionComponent<WrapperProps> = ( props: WrapperProps ): React.ReactElement<WrapperProps> => { const [user, setUser] = React.useState<User>(); const [room, setRoom] = React.useState<Room>(); const [round, setRound] = React.useState<Round>(); const content = ( <BrowserRouter data-test="wrapper-router"> <Provider store={store} data-test="wrapper-store-provider"> <SocketProvider> <UserContext.Provider value={{ user, setUser }}> <RoomContext.Provider value={{ room, setRoom }}> <RoundContext.Provider value={{ round, setRound }}> <Box sx={{ fontFamily: "main", height: "100vh", width: "100vw", position: "relative", }} > <Routes data-test="wrapper-routes">{props.children}</Routes> </Box> </RoundContext.Provider> </RoomContext.Provider> </UserContext.Provider> </SocketProvider> </Provider> </BrowserRouter> ); return process.env.NODE_ENV === 'development' ? content : <React.StrictMode>{content}</React.StrictMode>; };
内容的提问来源于stack exchange,提问作者Tsabary
相关产品推荐
相关产品推荐

