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

React接收WebSocket消息时状态重置为默认值的问题求助

React WebSocket接收消息时状态重置为初始值问题解决

我正在开发一个使用WebSocket接收实时更新的React项目,但遇到一个问题:每次收到WebSocket消息时,组件状态都会重置为默认值。最初用useState钩子,之后改用useReducer,两种情况均出现该问题。已确认dispatch后状态会更新,但收到消息时状态又会回到初始值。

Reducer代码

const initialState = {
  name: "",
  message: "",
  word: "",
  color: "#000000",
  eraseMode: false,
  strokeWidth: 2,
  eraserWidth: 10,
  isDrawing: false,
  id: null,
  turnID: null,
  names: null,
  scores: null,
  msgArr: [],
}

function reducer(state, action) {
  switch (action.type) {
    case "setAll":
      return {
        ...state,
        id: action.payload.id,
        turnID: action.payload.turnID,
        names: action.payload.names,
        scores: action.payload.scores,
      }
    case "setName":
      return { ...state, name: action.payload }
    case "setMessage":
      return { ...state, message: action.payload }
    case "setWord":
      return { ...state, word: action.payload }
    case "setColor":
      return { ...state, color: action.payload }
    case "setErasemode":
      return { ...state, eraseMode: action.payload }
    case "setStrokeWidth":
      return { ...state, strokeWidth: action.payload }
    case "setEraserWidth":
      return { ...state, eraserWidth: action.payload }
    case "setIsDrawing":
      return { ...state, isDrawing: action.payload }

    case "setId":
      return { ...state, id: action.payload }
    case "setTurnID":
      return { ...state, turnID: action.payload }
    case "setNames":
      return { ...state, names: action.payload }
    case "setScores":
      return { ...state, scores: action.payload }
    case "addMsg":
      return { ...state, msgArr: [...state.msgArr, action.payload] }

    default:
      throw new Error()
  }
}

函数组件代码

const [state, dispatch] = useReducer(reducer, initialState)
const ws = useRef(null)
const url = "ws://localhost:9833"
const p = useParams()

  useEffect(() => {
    // Initialize WebSocket
    const connectWebSocket = () => {
      if (ws.current) {
        ws.current.onerror = ws.current.onopen = ws.current.onclose = null
        ws.current.close()
      }
      ws.current = new WebSocket(`${url}/${p.roomID}`)
      console.log("Attempting to connect to WebSocket:", `${url}/${p.roomID}`)

      ws.current.onopen = () => {
        console.log("WebSocket connection opened")
      }

      ws.current.onerror = (error) => {
        console.error("WebSocket error:", error)
      }

      ws.current.onclose = () => {
        console.log("WebSocket connection closed")
        // Attempt to reconnect in case of a connection failure
        toast.error("Connection lost!")
        ws.current = null
        dispatch({ type: "setId", payload: null })
        setTimeout(() => connectWebSocket(), 1000)
      }

      ws.current.onmessage = (message) => {
        console.log("when WebSocket message received:", state)
        //here state is always equal to initial state
        const msg = JSON.parse(message.data)
        switch (msg.type) {
          case 0:
            if (msg.turnID === msg.id) {
              dispatch({ type: "setWord", payload: msg.word })
            }
            dispatch({
              type: "setAll",
              payload: {
                id: msg.id,
                turnID: msg.turnID,
                names: msg.names,
                scores: msg.scores,
              },
            })
            break
          case 1:
            break
          case 3:
            if (msg.isTrue) {
              dispatch({
                type: "addMsg",
                payload: `G ${state.names[msg.id]} guessed the word!👏👏`,
              })
              dispatch({ type: "setScores", payload: msg.scores })
            } else {
              dispatch({
                type: "addMsg",
                payload: `B ${state.names[msg.id]}: ${msg.message}`,
              })
            }
            break

          case 4:
            dispatch({
              type: "addMsg",
              payload: `S ${state.names[msg.turnID]} is drawing now.`,
            })
            dispatch({ type: "setTurnID", payload: msg.turnID })
            if (msg.turnID === state.id)
              dispatch({ type: "setWord", payload: msg.word })
            break

          case 6:
            dispatch({
              type: "addMsg",
              payload: `O ${msg.name} joined the room.`,
            })
            dispatch({ type: "setScores", payload: msg.scores })
            dispatch({ type: "setNames", payload: msg.names })
            break

          case 7:
            dispatch({
              type: "addMsg",
              payload: `R ${state.names[msg.id]} left the room.`,
            })
            dispatch({ type: "setScores", payload: msg.scores })
            dispatch({ type: "setNames", payload: msg.names })
            break

          default:
            break
        }
      }
    }

    connectWebSocket()

    // Cleanup on component unmount
    return () => {
      if (ws.current) {
        ws.current.onerror = ws.current.onopen = ws.current.onclose = null
        ws.current.close()
      }
    }
  }, [p.roomID])

问题原因

问题出在闭包捕获上:useEffect仅在p.roomID变化时执行,此时创建的onmessage回调会捕获当前的state(也就是初始状态)。后续组件状态更新时,这个回调并不会重新创建,所以它始终访问的是初始的state值,导致你看到状态重置的假象。

修复方案

使用useRef保存最新的状态引用,每次状态更新时同步到ref中,这样onmessage回调就能获取到最新的状态:

步骤1:添加状态引用

在组件中新增一个ref来跟踪最新状态:

const [state, dispatch] = useReducer(reducer, initialState)
const stateRef = useRef(state) // 保存最新状态的引用

// 每次state更新时同步到ref
useEffect(() => {
  stateRef.current = state
}, [state])

步骤2:修改onmessage回调

将回调中所有直接使用state的地方替换为stateRef.current:

ws.current.onmessage = (message) => {
  console.log("when WebSocket message received:", stateRef.current)
  const msg = JSON.parse(message.data)
  switch (msg.type) {
    // ...其他case保持不变
    case 3:
      if (msg.isTrue) {
        dispatch({
          type: "addMsg",
          payload: `G ${stateRef.current.names[msg.id]} guessed the word!👏👏`,
        })
        dispatch({ type: "setScores", payload: msg.scores })
      } else {
        dispatch({
          type: "addMsg",
          payload: `B ${stateRef.current.names[msg.id]}: ${msg.message}`,
        })
      }
      break
    case 4:
      dispatch({
        type: "addMsg",
        payload: `S ${stateRef.current.names[msg.turnID]} is drawing now.`,
      })
      dispatch({ type: "setTurnID", payload: msg.turnID })
      if (msg.turnID === stateRef.current.id)
        dispatch({ type: "setWord", payload: msg.word })
      break
    case 7:
      dispatch({
        type: "addMsg",
        payload: `R ${stateRef.current.names[msg.id]} left the room.`,
      })
      dispatch({ type: "setScores", payload: msg.scores })
      dispatch({ type: "setNames", payload: msg.names })
      break
    // ...其他case保持不变
  }
}

为什么这样有效?

useRef的current属性是可变的,不会受组件重新渲染的影响。我们通过useEffect在每次state更新时同步最新值到stateRef.current,这样onmessage回调就能随时访问到最新的组件状态,而不是被闭包捕获的初始值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 23:30:14