Expo Router登录重定向崩溃:iOS模态视图控制器重复展示问题
问题核心
在_layout.tsx中基于用户认证状态做路由分发:已认证用户进入(app)标签页布局,未认证用户显示欢迎页。登录后跳转至标签页首屏时应用崩溃,重启后因本地保存的token可正常加载标签页。Xcode报错:
Application tried to present modally a view controller <RCTModalHostViewController: 0x11c4632f0> that is already being presented by <UIViewController: 0x107b0b7a0>.
该错误说明登录跳转过程中,存在模态视图(如登录弹窗、加载弹窗)未完全关闭时,代码又触发了新的导航动作,导致iOS视图控制器层级冲突。
具体修复思路
1. 规范_layout.tsx的重定向逻辑,避免重复触发
_layout.tsx会在路由变化时重新渲染,直接无差别触发重定向会导致状态更新时多次执行导航。需加上状态判断和路由校验:
// _layout.tsx import { useEffect } from 'react'; import { useRouter, Redirect, Slot } from 'expo-router'; import { useAuth } from '../context/AuthContext'; import LoadingScreen from './LoadingScreen'; export default function RootLayout() { const { isAuthenticated, loading } = useAuth(); const router = useRouter(); useEffect(() => { if (!loading) { // 仅在当前路由不符合目标路由时触发跳转 const currentPath = window.location.pathname; if (isAuthenticated && !currentPath.startsWith('/app')) { router.replace('/app'); } else if (!isAuthenticated && currentPath !== '/welcome') { router.replace('/welcome'); } } }, [isAuthenticated, loading]); // 状态未就绪时显示加载页,避免布局闪烁和重复导航 if (loading) { return <LoadingScreen />; } // 未认证用户直接重定向到欢迎页 if (!isAuthenticated) { return <Redirect href="/welcome" />; } return <Slot />; }
关键:必须等待loading状态结束后再处理重定向,避免在token初始化(从AsyncStorage读取)过程中触发无效导航。
2. 登录页跳转逻辑:避免模态与路由动作冲突
如果登录页是以模态形式展示的,登录成功后直接用router.replace替换整个路由栈,不要同时执行模态关闭和路由跳转:
// LoginPage.tsx import { useAuth } from '../context/AuthContext'; import { useRouter } from 'expo-router'; export default function LoginPage() { const { login } = useAuth(); const router = useRouter(); const handleLogin = async (credentials) => { try { await login(credentials); // 替换路由栈到标签页首页,自动销毁之前的模态/欢迎页路由 router.replace('/app/home'); } catch (err) { // 处理登录错误 } }; return ( // 登录表单内容 ); }
不要手动调用Modal.close(),路由替换会自动清理之前的视图层级,避免动画冲突导致的控制器重复展示问题。
3. 优化AuthContext状态更新,避免重复触发重渲染
确保登录成功后isAuthenticated状态只更新一次,避免因状态多次变化导致_layout.tsx重复执行重定向:
// AuthContext.tsx import { createContext, useContext, useState, useEffect } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; const AuthContext = createContext(); export function AuthProvider({ children }) { const [isAuthenticated, setIsAuthenticated] = useState(false); const [loading, setLoading] = useState(true); // 初始化时读取本地token useEffect(() => { const initAuth = async () => { const token = await AsyncStorage.getItem('token'); setIsAuthenticated(!!token); setLoading(false); }; initAuth(); }, []); const login = async (credentials) => { setLoading(true); try { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials), }); const data = await response.json(); await AsyncStorage.setItem('token', data.token); // 仅在登录成功后更新一次认证状态 setIsAuthenticated(true); } catch (err) { console.error('Login failed:', err); } finally { setLoading(false); } }; return ( <AuthContext.Provider value={{ isAuthenticated, loading, login }}> {children} </AuthContext.Provider> ); } export const useAuth = () => useContext(AuthContext);
4. 排查模态组件的使用
如果欢迎页或登录页使用了Modal组件,检查以下点:
- 模态的
visible状态是否与认证状态正确绑定,避免状态更新时模态重复渲染 - 是否存在多个模态组件同时展示的情况
- 确保模态在路由跳转前已完全关闭(优先用路由替换代替手动关闭模态)
测试步骤
- 登录时观察
isAuthenticated和loading状态的变化,确保状态只更新一次 - 登录成功后使用
router.replace跳转,验证是否还会出现控制器冲突错误 - 重启应用,确认已保存token时能正常进入标签页
内容的提问来源于stack exchange,提问作者BigJobbies

