React类组件:修复“无法在已卸载组件更新状态”错误
解决React组件卸载后执行状态更新的错误
错误原因
当用户选择“Join a Game”并点击Submit后,组件将didGetUserName设为true,触发<Redirect>跳转,此时CreateNewGame组件会被卸载。但之前在componentDidMount中注册的socket.on("addNewRoom")事件监听并未被移除,当后端返回addNewRoom事件时,回调函数里的this.setState会尝试在已卸载的组件上更新状态,从而引发错误。
解决方案
需要做两个关键处理:
- 在组件卸载时移除socket事件监听,避免无效回调执行
- 添加组件挂载状态标志,确保只有组件处于挂载状态时才执行状态更新
修改后的代码
import React from "react"; import { Redirect } from "react-router-dom"; import uuid from "uuid/v4"; import { ColorContext } from "../context/colorcontext"; const socket = require("../connection/socket").socket; /** * Onboard is where we create the game room. */ class CreateNewGame extends React.Component { state = { didGetUserName: false, inputText: "", gameId: "", }; constructor(props) { super(props); this.textArea = React.createRef(); this.textArea1 = React.createRef(); // 添加标志位跟踪组件是否挂载 this._isMounted = false; // 保存socket回调的引用,方便后续移除监听 this.handleAddNewRoom = this.handleAddNewRoom.bind(this); } handleAddNewRoom = (theroomId) => { console.log(theroomId.rm); socket.emit("createNewGame", theroomId.rm + "1"); // 仅当组件挂载时才更新状态 if (this._isMounted) { this.setState({ gameId: theroomId.rm + "1", }); } }; sendJoinTournament = () => { const url = document.getElementById("urlBox").value; const roomId = url.slice(27, 63); socket.emit("checkRoomSize", roomId); if (this._isMounted) { this.setState({ gameId: roomId, }); } }; componentDidMount() { this._isMounted = true; // 使用保存的回调引用注册事件 socket.on("addNewRoom", this.handleAddNewRoom); } componentWillUnmount() { this._isMounted = false; // 移除socket事件监听 socket.off("addNewRoom", this.handleAddNewRoom); } sendCreateNewGame = () => { const newGameRoomId = uuid(); if (this._isMounted) { this.setState({ gameId: newGameRoomId, }); } socket.emit("createNewGame", newGameRoomId); }; typingUserName = () => { const typedText = this.textArea.current.value; if (this._isMounted) { this.setState({ inputText: typedText, }); } }; render() { return ( <React.Fragment> {this.state.didGetUserName ? ( <Redirect to={"/game/" + this.state.gameId}> <button className="btn btn-success" style={{ marginLeft: String(window.innerWidth / 2 - 60) + "px", width: "120px", }} > Start Game </button> </Redirect> ) : ( <div> <h1 style={{ textAlign: "center", marginTop: String(window.innerHeight / 3) + "px", }} > Your Username: </h1> <input style={{ marginLeft: String(window.innerWidth / 2 - 120) + "px", width: "240px", marginTop: "62px", }} ref={this.textArea} onInput={this.typingUserName} ></input> <br></br> <input style={{ marginLeft: String(window.innerWidth / 2 - 120) + "px", width: "10px", marginTop: "31px", }} type="radio" id="myRadio" name="createRadio" value="HTML" ref={this.radiobutton1} ></input> <label htmlFor="html">Create New Game</label> <br></br> <input style={{ marginLeft: String(window.innerWidth / 2 - 120) + "px", width: "10px", marginTop: "31px", }} type="radio" id="myRadio1" name="joinRadio" value="HTML" ref={this.radiobutton2} ></input> <label htmlFor="html">Join a Game</label> <input style={{ marginLeft: String(window.innerWidth / 2 - 120) + "px", width: "240px", marginTop: "62px", }} placeholder="paste link" id="urlBox" ref={this.textArea1} ></input> <button className="btn btn-primary" style={{ marginLeft: String(window.innerWidth / 2 - 60) + "px", width: "120px", marginTop: "62px", }} disabled={!(this.state.inputText.length > 0)} onClick={() => { if (document.getElementById("myRadio").checked) { this.props.didRedirect(); this.props.setUserName(this.state.inputText); if (this._isMounted) { this.setState({ didGetUserName: true, }); } this.sendCreateNewGame(); } if (document.getElementById("myRadio1").checked) { this.sendJoinTournament(); if (this._isMounted) { this.setState({ didGetUserName: true, }); } } }} > Submit </button> </div> )} </React.Fragment> ); } } const Onboard = (props) => { const color = React.useContext(ColorContext); return ( <CreateNewGame didRedirect={color.playerDidRedirect} setUserName={props.setUserName} /> ); }; export default Onboard;
关键修改点说明
- 添加
_isMounted标志位,在componentDidMount中设为true,componentWillUnmount中设为false,所有setState调用前都检查该标志位 - 将
socket.on("addNewRoom")的回调函数提取为类方法handleAddNewRoom,并保存引用,在componentWillUnmount中用socket.off移除该监听,避免组件卸载后回调仍被触发 - 修正了代码中的小问题:将
Ref改为ref,label的for改为htmlFor(React中JSX的属性规范)
内容的提问来源于stack exchange,提问作者kylekode0
相关产品推荐
相关产品推荐

