Fix Expo Router Error: Attempted to navigate before mounting Root Layout component
If you are building a React Native application with Expo Router, you might encounter the following common error when attempting to handle authentication or initial redirects:
Attempted to navigate before mounting the Root Layout component. Ensure Root Layout component is rendering a Slot, or other navigator on the first render.
Even when using hooks like useRootNavigationState() to check if the root navigation state has a key, router.replace() can still throw this error during the initial render lifecycle. In this article, we'll explain why this happens and explore the recommended modern approach to handle protected routes and auth redirects in Expo Router.
Why This Error Happens
The root layout component (app/_layout.tsx) is responsible for setting up the top-level navigation provider (such as <Stack />, <Tabs />, or <Slot />). When React Native renders this component for the first time, the underlying React Navigation tree is still being mounted.
Calling imperative navigation methods like router.replace() or router.push() inside a useEffect at the root level often triggers execution before the navigation context is fully mounted and ready to consume actions. While checking useRootNavigationState() worked in earlier versions of Expo Router, relying on imperative navigation inside layout effects remains fragile and anti-pattern in modern Expo Router (v2+).
The Recommended Solution: Declarative Redirects
Instead of triggering imperative redirects via router.replace() inside a useEffect, Expo Router advocates for declarative navigation using the <Redirect /> component combined with layout route groups (e.g., (auth) and (app)).
1. Create an Authentication Context
Manage your auth state globally using a React Context provider inside your root layout:
// context/AuthContext.tsx
import React, { createContext, useContext, useState } from "react";
type AuthContextType = {
isAuthenticated: boolean;
signIn: () => void;
signOut: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
return (
<AuthContext.Provider
value={{
isAuthenticated,
signIn: () => setIsAuthenticated(true),
signOut: () => setIsAuthenticated(false),
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
2. Wrap Your Root Layout with the Provider
Keep your root layout focused purely on providing global context providers and rendering the top-level navigator:
// app/_layout.tsx
import { Stack } from "expo-router";
import { AuthProvider } from "@/context/AuthContext";
export default function RootLayout() {
return (
<AuthProvider>
<Stack screenOptions={{ headerShown: false }} />
</AuthProvider>
);
}
3. Handle Navigation Declaratively in Sub-Layouts
Organize your routes into group directories such as app/(app)/_layout.tsx for protected routes and app/(auth)/_layout.tsx for public/login routes. Protect the private route group using the <Redirect /> component:
// app/(app)/_layout.tsx
import { Redirect, Stack } from "expo-router";
import { useAuth } from "@/context/AuthContext";
export default function AppLayout() {
const { isAuthenticated } = useAuth();
// Declaratively redirect to auth group if user is not authenticated
if (!isAuthenticated) {
return <Redirect href="/(auth)" />;
}
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Home" }} />
</Stack>
);
}
Summary
- Avoid imperative routing (
router.replace()insideuseEffect) at the root level during initial mounting. - Do not rely on
useRootNavigationState()for authentication redirects; it is legacy behavior. - Use
<Redirect />declaratively within route groups or sub-layouts to safely steer unauthenticated users to login screens without timing errors.