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

Expo Router登录重定向崩溃:iOS模态视图控制器重复展示问题

Expo Router 登录后跳转崩溃:模态视图控制器重复展示问题修复思路

问题核心

在_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状态是否与认证状态正确绑定,避免状态更新时模态重复渲染
  • 是否存在多个模态组件同时展示的情况
  • 确保模态在路由跳转前已完全关闭(优先用路由替换代替手动关闭模态)

测试步骤

  1. 登录时观察isAuthenticated和loading状态的变化,确保状态只更新一次
  2. 登录成功后使用router.replace跳转,验证是否还会出现控制器冲突错误
  3. 重启应用,确认已保存token时能正常进入标签页

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 19:16:29