React登录切换页面报未挂载组件状态更新警告如何解决
问题描述
开发过程中遇到 Uncaught (in promise) DOMException: Permission denied Error 报错,相关业务逻辑如下:
- 登录操作执行时,会将全局登录状态值修改为
true,响应式变量与登录方法代码如下:
export const isLoggedInVar = makeVar(false); export const tokenVar = makeVar(""); export const logUserIn = async (token) => { await AsyncStorage.multiSet([ ["token", JSON.stringify(token)], ["loggedIn", JSON.stringify("yes")], ]); tokenVar(token); isLoggedInVar(true); // 全局登录状态更新点 };
- 当
isLoggedIn状态变为true时,Tab路由会从Profile_Logout(未登录态页面)切换为Profile_Login(已登录态页面),路由配置代码如下:
<Tab.Screen name="Profile" component={isLoggedIn ? Profile_Login : Profile_Logout} />
运行时持续抛出如下警告:
❌ Warning: 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 %s.%s, a useEffect cleanup function,
已知该警告的表层触发原因是组件卸载后仍有代码尝试执行React状态更新,现有三个具体疑问:
- 逻辑上是状态变更触发页面切换,为什么会出现页面切换完成后仍触发状态更新的报错?
- 警告提示需要在
useEffect中编写清理函数解决问题,该场景下清理函数的具体作用是什么、正确编写方式是怎样的? - 清理逻辑应该写在切换后的
Profile_Login页面,还是切换前的Profile_Logout页面中?
解答
1. 报错触发的根本原因
这个时序矛盾本质是异步任务执行顺序和React组件卸载时机错位导致的:
logUserIn是异步函数,await AsyncStorage.multiSet执行的等待间隙,React已经完成了路由切换、旧组件(Profile_Logout)的卸载流程- 你在
Profile_Logout组件内部大概率写了监听登录状态、或者绑定登录操作后更新组件本地state的逻辑,这些异步回调、状态订阅没有在组件卸载时终止。等异步操作resolve后执行状态更新时,对应的Profile_Logout组件已经被卸载,就会抛出该警告 - 报错和你修改全局
isLoggedInVar的操作无关,问题根源是被卸载的旧组件内部残留的未收尾异步任务、状态订阅。
2. 清理函数的作用与编写方式
useEffect返回的清理函数,核心作用是在组件卸载、或者effect依赖变更重新执行前,清除当前effect绑定的所有副作用,避免卸载后残留的回调继续操作已销毁的组件实例。
针对这个登录场景,常用的清理逻辑分两类:
- 清除订阅/事件监听:如果你在
Profile_Logout里监听了登录状态变化、或者绑定了自定义事件,清理函数中要主动取消订阅useEffect(() => { const handleLoginChange = () => { // 组件内部state更新逻辑 } // 绑定状态监听 const unsubscribe = isLoggedInVar.onNextChange(handleLoginChange); // 清理函数:组件卸载时取消监听 return () => { unsubscribe(); } }, []) - 挂载标记阻断异步回调:如果登录请求、AsyncStorage操作是在
Profile_Logout组件内部触发的,用标记位记录组件挂载状态,异步回调执行前先判断组件是否还存在,再决定要不要更新stateuseEffect(() => { let isMounted = true; const handleLogin = async () => { await loginApiRequest(); // 仅组件未卸载时执行本地状态更新 if (isMounted) { setLoginLoading(false); } } handleLogin(); return () => { // 组件卸载时修改标记位 isMounted = false; } }, [])
3. 清理逻辑的编写位置
清理逻辑写在切换前会被卸载的Profile_Logout页面中:
- 警告的本质是「已经被销毁的组件,仍有代码尝试修改它的内部状态」,哪个组件会被卸载、哪个组件内部定义了更新自身state的异步任务/订阅,清理逻辑就写在哪个组件里
- 切换后新挂载的
Profile_Login是状态变更完成后才渲染的,不存在卸载后被更新状态的问题,不需要针对这次切换写清理逻辑。
内容的提问来源于stack exchange,提问作者Hyejung
相关产品推荐
相关产品推荐

