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

非StrictMode下React useEffect执行两次问题求助

Socket应用中useEffect执行两次的问题排查与解决

问题描述

我正在开发一个基于Socket的应用,遇到了useEffect执行两次的问题,这引发了竞态、重复emit和重复注册的情况。将包含emit逻辑的页面简化为测试页后,问题依然存在。检查代码发现:

  • index.tsx未启用StrictMode
  • Wrapper组件内部嵌套了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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 01:45:35