React卸载组件后状态更新报错:isMounted检测无效求助
我在React应用中用axios从API获取数据,需要在多个嵌套组件中展示。相关代码如下:
const Building = () => { const { bid } = useParams(); const { userAccessToken } = useAuth(); const [buildingData, setBuildingData] = useState([]); const bearerToken = `Bearer ${userAccessToken}`; React.useEffect(() => { let isMounted = true; const axiosConfig = { headers: { Authorization: bearerToken }, }; axios .get( "http://localhost:3001/building?requestedlid=2&requestedbid=" + bid, axiosConfig ) .then(function (response) { if (isMounted) { setBuildingData(response.data[0]); } }) .catch(function (error) { // handle error console.log(error); }); return () => { isMounted = false; }; }, [bearerToken, bid]); return ( <React.Fragment> <Helmet title="Building Profile" /> <Container fluid className="p-0"> <Breadcrumb className="float-end mt-2"> <Breadcrumb.Item href="/dashboard/default">Home</Breadcrumb.Item> <Breadcrumb.Item href="/buildings/portfolio"> Portfolio </Breadcrumb.Item> <Breadcrumb.Item active>Building Profile</Breadcrumb.Item> </Breadcrumb> <h1 className="h3 mb-3"> Building Profile <OffcanvasHelp id="buildingprofile" name="Building Profile" scroll backdrop /> </h1> <div className="clearfix"></div> <Row> <Col xl="8"> <BuildingProfile name={buildingData.building_name} status={buildingData.status} description={buildingData.description} keycontacts={buildingData.key_contacts} created={buildingData.stats.created_date} golive={buildingData.stats.golive_date} /> <Rooms /> </Col> <Col xl="4"> <AccountManager /> <Map location={buildingData.location} /> <GetSupport type="commercial" /> </Col> </Row> </Container> </React.Fragment> ); };
遇到的错误:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
我尝试了StackOverflow提到的isMounted布尔值检测组件挂载状态,但问题没解决。奇怪的是,只有给BuildingProfile组件传递以下两个props时才触发错误:
created={buildingData.stats.created_date} golive={buildingData.stats.golive_date}
不传递这两个props的话,其他功能都正常。肯定遗漏了某些细节,求解决思路或提示。
更新:附上BuildingProfile组件代码
const BuildingProfile = ({ name, status, description, created, golive, keycontacts, }) => { // Modal config for "Deactivate Building" const initOpenModals = () => { let modals = {}; colors.forEach((color, index) => { modals = Object.assign({}, modals, { [index]: false }); }); console.log(modals); return modals; }; const [openModals, setOpenModals] = useState(() => initOpenModals()); const toggle = (index) => { // Toggle selected element setOpenModals((openModals) => Object.assign({}, openModals, { [index]: !openModals[index] }) ); }; const notyf = useContext(NotyfContext); const [type] = useState("success"); const [duration] = useState("5000"); const [ripple] = useState(true); const [dismissible] = useState(false); const [positionX] = useState("right"); const [positionY] = useState("top"); const navigate = useNavigate(); return ( <Card> <Card.Header className="mb-0 pb-0"> <Card.Title className="mb-0"> <IsAllowed to="edit:buildings"> <div className="card-actions float-end"> <Dropdown align="end"> <Dropdown.Toggle as="a" bsPrefix="-"> <MoreHorizontal /> </Dropdown.Toggle> <Dropdown.Menu> <Dropdown.Item onClick={() => navigate("/buildings/edit")}> Edit Building </Dropdown.Item> <React.Fragment key="deactivateBuilding"> <Dropdown.Item onClick={() => toggle("deactivateBuilding")}> Deactivate Building </Dropdown.Item> <Modal show={openModals["deactivateBuilding"]} onHide={() => toggle("deactivateBuilding")} centered > <Modal.Header closeButton> <b>Admin Function:</b> Deactivate Building </Modal.Header> <Modal.Body className="m-3"> <p className="text-left mb-0"> Are you sure you want to deactivate the <b>Bus Works</b> building? This will prevent the building from showing up in the platform completely. </p> </Modal.Body> <Modal.Footer> <Button variant="secondary" onClick={() => toggle("deactivateBuilding")} > Close </Button>{" "} <Button variant="danger" onClick={() => { toggle("deactivateBuilding"); notyf.open({ type, message: "The building has been deactivated.", duration, ripple, dismissible, position: { x: positionX, y: positionY, }, }); }} > Deactivate Building </Button> </Modal.Footer> </Modal> </React.Fragment> </Dropdown.Menu> </Dropdown> </div> </IsAllowed> <h1 className="mb-0 pb-0">{name}</h1> <Badge className={ status === "Live Building" ? "my-2 btn-gradient inline" : "my-2 inline" } bg="success" > {status} </Badge> </Card.Title> </Card.Header> <Card.Body> <h5>Building Overview:</h5> <p className="mb-4">{description}</p> <div className="row"> <div className="col-md-4"> <div className="mb-4"> <h5>Created Date</h5> <p>{created}</p> </div> </div> <div className="col-md-4"> <div className="mb-4"> <h5>Go-Live Date</h5> <p>{golive}</p> </div> </div> <div className="col-md-4"> <div className="mb-4"> <h5>Key Contacts</h5> <div> <span className="me-1"> <OverlayTrigger placement="right" overlay={ <Tooltip id="tooltip-right">Joe Bloggs</Tooltip> } > <img src={avatar3} width="28" height="28" className="rounded-circle me-2" alt="Joe Bloggs" /> </OverlayTrigger> </span> <span className="me-1"> <OverlayTrigger placement="right" overlay={ <Tooltip id="tooltip-right">Joe Bloggs</Tooltip> } > <img src={avatar2} width="28" height="28" className="rounded-circle me-2" alt="Joe Bloggs" /> </OverlayTrigger> </span> <span className="me-1"> <OverlayTrigger placement="right" overlay={ <Tooltip id="tooltip-right">Joe Bloggs</Tooltip> } > <img src={avatar1} width="28" height="28" className="rounded-circle me-2" alt="Joe Bloggs" /> </OverlayTrigger> </span> <Link to="#" className="d-inline-block text-muted fw-bold ms-2"> +2 more </Link> </div> </div> </div> </div> </Card.Body> </Card> ); };
1. 修复初始状态与嵌套属性访问问题
你的buildingData初始值是数组[],但API返回的是对象response.data[0]。组件首次渲染时,buildingData.stats会直接报错(数组没有stats属性),这会导致组件渲染失败甚至提前卸载,而此时axios请求仍在进行,请求完成后执行setBuildingData就会触发"卸载组件更新状态"的错误。
两种修复方式:
- 修改初始状态为对象:
const [buildingData, setBuildingData] = useState({}); - 传递props时用可选链处理嵌套属性:
created={buildingData.stats?.created_date} golive={buildingData.stats?.golive_date}
2. 替换isMounted为axios请求取消机制
isMounted是React官方不推荐的反模式,正确做法是主动取消未完成的axios请求,推荐使用AbortController(CancelToken已被废弃):
React.useEffect(() => { const abortController = new AbortController(); const axiosConfig = { headers: { Authorization: bearerToken }, signal: abortController.signal // 绑定取消信号 }; axios .get( `http://localhost:3001/building?requestedlid=2&requestedbid=${bid}`, axiosConfig ) .then(function (response) { setBuildingData(response.data[0]); }) .catch(function (error) { if (!axios.isCancel(error)) { // 排除主动取消的错误 console.log(error); } }); return () => { abortController.abort(); // 组件卸载时取消请求 }; }, [bearerToken, bid]);
3. 排查BuildingProfile组件的潜在问题
检查initOpenModals中用到的colors变量,如果它是外部可变变量,可能导致初始状态计算异常;另外确保toggle方法的函数式更新逻辑没有问题,不过这和当前错误关联度较低,可作为后续排查点。
总结
最可能的根源是初始状态不匹配导致的渲染错误,引发组件提前卸载,进而触发axios请求完成后的状态更新错误。先修复初始状态或嵌套属性访问,再用AbortController取消请求,即可解决问题。
内容的提问来源于stack exchange,提问作者Jonathan Nathanson

