Closed andrew-levy closed 1 year ago
Yeah, I'm having a similar issue:
Error: Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot, or other navigator on the first render.
app/_layout.tsx
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { Stack, SplashScreen, Slot } from "expo-router";
import { useFonts } from 'expo-font';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { SafeAreaProvider } from "react-native-safe-area-context";
import {
Auth,
FlashMessageWrapper
} from "@components";
import { Provider } from "@store";
SplashScreen.preventAutoHideAsync();
export default function Layout() {
const [loaded, error] = useFonts({
poppins: require('@assets/fonts/Poppins-Regular.ttf'),
"poppins-light": require('@assets/fonts/Poppins-Light.ttf'),
"poppins-medium": require('@assets/fonts/Poppins-Medium.ttf'),
"poppins-bold": require('@assets/fonts/Poppins-Bold.ttf'),
"poppins-semibold": require('@assets/fonts/Poppins-SemiBold.ttf'),
...FontAwesome.font,
});
useEffect(() => {
if (error) throw error;
}, [error]);
if (loaded) {
SplashScreen.hideAsync();
}
if (!loaded) {
return null;
}
return (
<Provider>
<FlashMessageWrapper />
<Auth>
<SafeAreaProvider>
<View style={{ flex: 1, }}>
<Slot />
</View>
</SafeAreaProvider>
</Auth>
</Provider>
);
}
app/index.tsx
import { Redirect } from "expo-router";
export default function Index() {
return <Redirect href={"/home"} />
}
But if I change the return
of the Layout from null
to <Slot />
works but gives me a warning about the fonts used before they were loaded.
from:
if (!loaded) {
return null;
}
to: (this is not a solution)
if (!loaded) {
return <Slot />;
}
Yep, I'm having this issue as well, I have a redux provider at the root layout.
In my case, I have a <Redirect />
component in the index.tsx, and the error appears if the condition is true:
app/index.tsx
const LandingIndexPage = () => {
const { user } = useUser();
if (user) {
return <Redirect href="/home" />; <- this will cause the error
}
// This works
return (
<View style={[styles.container]}>
<SafeAreaView style={styles.contentContainer}>
// ... stuff
</SafeAreaView>
</View>
);
};
If there is no user, the app loads and no error. But once I log in and refresh on the console, I see Error: Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot
Hello, same here. I got the same error after upgrading expo to sdk 49 and expo router to version 2 I played a bit and I was able to have this working, but I'm not sure if this is a right way. To get this working I did something like this to the useProtectedRoute
function useProtectedRoute() {
...
const [isNavigationReady, setNavigationReady] = useState(false);
useEffect(() => {
const unsubscribe = rootNavigation?.addListener('state', (event) => {
// console.log("INFO: rootNavigation?.addListener('state')", event);
setNavigationReady(true);
});
return function cleanup() {
if (unsubscribe) {
unsubscribe();
}
};
}, [rootNavigation]);
useEffect(() => {
if (!isNavigationReady) {
return;
}
... do the rest
}
}
Hello, same here. I got the same error after upgrading expo to sdk 49 and expo router to version 2 I played a bit and I was able to have this working, but I'm not sure if this is a right way. To get this working I did something like this to the useProtectedRoute
function useProtectedRoute() { ... const [isNavigationReady, setNavigationReady] = useState(false); useEffect(() => { const unsubscribe = rootNavigation?.addListener('state', (event) => { // console.log("INFO: rootNavigation?.addListener('state')", event); setNavigationReady(true); }); return function cleanup() { if (unsubscribe) { unsubscribe(); } }; }, [rootNavigation]); useEffect(() => { if (!isNavigationReady) { return; } ... do the rest } }
Thanks @GoldMyr1994 for sharing the approach. It didn't work out for me just out of the box because the main page is loaded without user logged in because of the early return of if (!isNavigationReady)
But it seems promising.
Hello, same here. I got the same error after upgrading expo to sdk 49 and expo router to version 2 I played a bit and I was able to have this working, but I'm not sure if this is a right way. To get this working I did something like this to the useProtectedRoute
function useProtectedRoute() { ... const [isNavigationReady, setNavigationReady] = useState(false); useEffect(() => { const unsubscribe = rootNavigation?.addListener('state', (event) => { // console.log("INFO: rootNavigation?.addListener('state')", event); setNavigationReady(true); }); return function cleanup() { if (unsubscribe) { unsubscribe(); } }; }, [rootNavigation]); useEffect(() => { if (!isNavigationReady) { return; } ... do the rest } }
Worked for me. I have a AuthProvider using the useProctedRoute hooks that is checking if the session/user exists to session wether to redirect to the Auth or Home screen. Using the above before the router.replace helped remove the error. Again not sure if this is the appropriate solution dealing with auth routes but it works for now.
@sbkl Yes, me too not sure if this is the appropriate solution
I tried before with isReady()
but this did not worked for me.
const rootNavigation = useRootNavigation();
...
useEffect( () =>
if (!rootNavigation?.isReady()) {
return;
}
In my case I have an app folder like this
I think that the nested drawer layout is not ready. I trid to navigate to '/' it is ok
In the end I ended up with this https://github.com/expo/router/issues/740#issuecomment-1625033355
@fmendez89 not sure if I have understood
It didn't workout for me just out of the box because the main page is loaded without user logged in because of the early return of if (!isNavigationReady)
I have this in my provider to wait
<AuthenticationContext.Provider ...>
{state.isLoading ? (
<View style={{ flex: 1, justifyContent: 'center' }}>
<ActivityIndicator size="large" />
</View>
) : (
props.children
)}
</AuthenticationContext.Provider>
Same issue
Have hit the same thing upgrading from Expo-Router V1, and React Navigation before that. I guess the same useFonts
example that many people were following.
This is my current workaround to avoid navigating before the navigator is ready. It also avoids having to add a listener to the navigation state.
Seems to work, until a neater solution is found.
//index.tsx
import { useRootNavigationState, Redirect } from 'expo-router';
export default function App() {
const rootNavigationState = useRootNavigationState();
if (!rootNavigationState?.key) return null;
return <Redirect href={'/test'} />
}
I have the same issue
SplashScreen.preventAutoHideAsync();
export default function Root() {
const [fontsLoaded] = useFonts({
"Inter-Black": require("../assets/fonts/Inter-Black.ttf") as Font,
"Inter-Bold": require("../assets/fonts/Inter-Bold.ttf") as Font,
"Inter-ExtraBold": require("../assets/fonts/Inter-ExtraBold.ttf") as Font,
"Inter-ExtraLight": require("../assets/fonts/Inter-ExtraLight.ttf") as Font,
"Inter-Light": require("../assets/fonts/Inter-Light.ttf") as Font,
"Inter-Medium": require("../assets/fonts/Inter-Medium.ttf") as Font,
"Inter-Regular": require("../assets/fonts/Inter-Regular.ttf") as Font,
"Inter-SemiBold": require("../assets/fonts/Inter-SemiBold.ttf") as Font,
"Inter-Thin": require("../assets/fonts/Inter-Thin.ttf") as Font,
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
return (
<BottomSheetModalProvider>
<SafeAreaProvider>
<ThemeProvider>
<AuthProvider>
<QueryClientProvider client={queryClient}>
<Stack screenOptions={{ headerShown: false }} />
</QueryClientProvider>
</AuthProvider>
</ThemeProvider>
</SafeAreaProvider>
</BottomSheetModalProvider>
);
}
If I put a <Slot />
instead of null in the if(!fontsLoaded)
I don't get the crash.
But I do see the redirection
We need the redirection before displaying the screens
Works in expo v1 but not in v2
Here is my app folder (home is a tab and the rest is stack)
and here is the AuthProvider
import { useRouter, useSegments } from "expo-router";
import {
ReactNode,
createContext,
useContext,
useEffect,
useState,
} from "react";
import { storage } from "../const";
import { storageKeys } from "../storageKeys";
type Props = {
children?: ReactNode;
};
type AuthContextType = {
user: unknown;
signIn: () => void;
signOut: () => void;
};
const AuthContext = createContext<AuthContextType | null>(null);
// This hook can be used to access the user info.
export function useAuth() {
return useContext(AuthContext);
}
// This hook will protect the route access based on user authentication.
function useProtectedRoute(user: unknown) {
const segments = useSegments();
const router = useRouter();
useEffect(() => {
const inAuthGroup = segments[0] === "auth";
if (
// If the user is not signed in and the initial segment is not anything in the auth group.
!user &&
!inAuthGroup
) {
// Redirect to the sign-in page.
router.replace("/auth/login");
} else if (user && inAuthGroup) {
// Redirect away from the sign-in page.
router.replace("/");
}
}, [user, segments, router]);
}
export function AuthProvider(props: Props) {
const [user, setUser] = useState<unknown>(
storage.getString(storageKeys.ISLOGGED)
);
useProtectedRoute(user);
return (
<AuthContext.Provider
value={{
signIn: () => {
storage.set(storageKeys.ISLOGGED, "true");
setUser("true");
},
signOut: () => {
storage.delete(storageKeys.ISLOGGED);
setUser(null);
},
user,
}}
>
{props.children}
</AuthContext.Provider>
);
}
Here is 2 videos One with the crash And one with the Slot
https://github.com/expo/router/assets/56580186/24747e01-5951-4177-ae68-a72c3145e8a7
https://github.com/expo/router/assets/56580186/a8818205-dbaa-488e-8332-bdf3f30f6cc1
Same issue, tried @mr-cactus solution and it works
As per the other issue #745 , the best workaround so far for me is https://github.com/expo/router/issues/745#issuecomment-1625406889
if (Platform.OS === "ios") {
setTimeout(() => {
router.replace("/");
}, 1)
} else {
setImmediate(() => {
router.replace("/");
});
}
Everything else just works as expected.
I have the same problem, I used setTimeout to redirect to the correct screen. Now it works, but it's not an elegant solution.
To solve the problem I changed the useEffect of the auth provider by adding at the top :
if (!rootNavigation?.key) {
return;
}
But then to fix the display problem if you're not logged in (see video) I had to add a timeout in the main layout. I don't particularly like this solution but I hope the problem will soon be solved.
useEffect(() => {
if (fontsLoaded) {
setTimeout(() => {
SplashScreen.hideAsync();
}, 1500);
}
}, [fontsLoaded]);
if (!fontsLoaded) {
return null;
}
https://github.com/expo/router/assets/56580186/24747e01-5951-4177-ae68-a72c3145e8a7
This solution will work, in reality it is doing the same thing as listening for the ready event using useRootNavigation() that I believe some on suggested earlier in the thread.
On Tue, Jul 11, 2023 at 11:50 AM Gino Dzin @.***> wrote:
To solve the problem I changed the useEffect of the auth provider by adding at the top :
if (!rootNavigation?.key) { return; }
But then to fix the display problem if you're not logged in (see video) I had to add a timeout in the main layout. I don't particularly like this solution but I hope the problem will soon be solved.
useEffect(() => { if (fontsLoaded) { setTimeout(() => { SplashScreen.hideAsync(); }, 1500); } }, [fontsLoaded]);
if (!fontsLoaded) { return null; }
— Reply to this email directly, view it on GitHub https://github.com/expo/router/issues/740#issuecomment-1631073448, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAFGOKQJ7HZQQLGJ452YDXPVY2ZANCNFSM6AAAAAA2ARJYTE . You are receiving this because you are subscribed to this thread.Message ID: @.***>
--
--
Aaron K. Saunders CEO Clearly Innovative Inc @.*** www.clearlyinnovative.com
This email message and any attachment(s) are for the sole use of the intended recipient(s) and may contain proprietary and/or confidential information which may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient(s), please contact the sender by reply email and destroy the original message and any copies of the message as well as any attachment(s) to the original message.
@aaronksaunders Sure, it works, but in my case it adds 1.5 seconds to my startup time.
Good to know, wonder what is going on in that 1.5 seconds. I have used both approaches in the past so it will be interesting to see what is considered the official approach
On Tue, Jul 11, 2023 at 12:09 PM Gino Dzin @.***> wrote:
@aaronksaunders https://github.com/aaronksaunders Sure, it works, but in my case it adds 1.5 seconds to my startup time.
— Reply to this email directly, view it on GitHub https://github.com/expo/router/issues/740#issuecomment-1631103861, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAFGKCY2FEZL3CY7ZC73TXPV3CBANCNFSM6AAAAAA2ARJYTE . You are receiving this because you were mentioned.Message ID: @.***>
--
--
Aaron K. Saunders CEO Clearly Innovative Inc @.*** www.clearlyinnovative.com
This email message and any attachment(s) are for the sole use of the intended recipient(s) and may contain proprietary and/or confidential information which may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient(s), please contact the sender by reply email and destroy the original message and any copies of the message as well as any attachment(s) to the original message.
dont understand why waiting for the listener to say "ready" is bad?
--
Aaron K. Saunders CEO Clearly Innovative Inc @.*** www.clearlyinnovative.com
This email message and any attachment(s) are for the sole use of the intended recipient(s) and may contain proprietary and/or confidential information which may be privileged or otherwise protected from disclosure. Any unauthorized review, use, disclosure or distribution is prohibited. If you are not the intended recipient(s), please contact the sender by reply email and destroy the original message and any copies of the message as well as any attachment(s) to the original message.
On Tue, Jul 18, 2023 at 8:49 AM Soucanye de Landevoisin < @.***> wrote:
Have hit the same thing upgrading from Expo-Router V1, and React Navigation before that. I guess the same useFonts example that many people were following.
This is my current workaround to avoid navigating before the navigator is ready. It also avoids having to add a listener to the navigation state.
Seems to work, until a neater solution is found.
//index.tsx
import { useRootNavigationState, Redirect } from 'expo-router';
export default function App() { const rootNavigationState = useRootNavigationState();
if (!rootNavigationState?.key) return null;
return <Redirect href={'/test'} /> }
Worked for me too. Just a thing, key doesn't exist on all states of rootNavigationState, so i use route for now. Still not a comfortable solution but it works
— Reply to this email directly, view it on GitHub https://github.com/expo/router/issues/740#issuecomment-1640152489, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAFGMQUPAHGFKDN2E2QNTXQ2A4RANCNFSM6AAAAAA2ARJYTE . You are receiving this because you were mentioned.Message ID: @.***>
Solved it with this monstrosity (hopefully #794 will make it obsolete)
function Layout() {
const router = useRouter();
const segments = useSegments();
const pathname = usePathname();
const rootNavigation = useRootNavigation();
const rootNavigationState = useRootNavigationState();
const navigation = useNavigation();
const [counter, setCounter] = useState(0);
useEffect(() => {
if (
!rootNavigation ||
rootNavigation.isReady() === false ||
!navigation ||
navigation.isReady() === false ||
!rootNavigationState ||
rootNavigationState.stale
) {
const timeoutId = setTimeout(() => setCounter(() => counter + 1), 500);
return () => clearTimeout(timeoutId);
}
// ... perform redirects
}, [counter, segments, pathname, ...]);
return (
<Stack screenOptions={{ headerShown: false, animation: "none" }} initialRouteName="index" />
);
}
export const unstable_settings = {
// ensures any route can link back to `/`
initialRouteName: "index",
};
It took me a long time to solve this problem when switching from expo-route v1 to v2 and here's how I solved it.
I added the following code to the MainContextProvider:
`const [isNavigationReady, setNavigationReady] = useState(false); const rootNavigation = useRootNavigation();
useEffect(() => {
const unsubscribe = rootNavigation?.addListener('state', () => {
setNavigationReady(true);
});
return function cleanup() {
if (unsubscribe) {
unsubscribe();
}
};
}, [rootNavigation]);`
And in useEffect, which is responsible for redirecting between pages, I added the following line:
if (!isNavigationReady) { return; }
Calling the useRootNavigation hook in my useProtected
hook make the app crash on the tabs screen. get an infinite call on a setState.
just by adding the line const r = useRootNavigation()
in my hook, i get this error/crash msg on ios when i navigate the tab screens.
weird thing btw, what actually happen is that the react-navigation doesn't see the tab screen anymore so only way to navigate to them is using deeplinks like npx uri-scheme /home
which then trigger the error you see in the screen.
EDIT:
looks like removing the unstable_settings at the root layout make the error disappear. The tab screen are still not listed in the app route in react-nav state but i don't get an error navigating to it. but i get a warning now
The `redirect` prop on <Screen /> is deprecated and will be removed. Please use `router.redirect` instead
@wcastand here is how I a using it
function useProtectedRoute(user) {
const segments = useSegments();
const router = useRouter();
const navigationState = useRootNavigationState();
React.useEffect(() => {
if (!navigationState?.key) return;
const inAuthGroup = segments[0] === "(auth)";
console.log("user", user);
if (
// If usernot signed in and the initial segment is not anything in the auth group.
!user &&
!inAuthGroup
) {
// Redirect to the login page.
router.replace("/login");
} else if (user && inAuthGroup) {
// Redirect away from the login page.
router.replace("/");
}
}, [user, segments, navigationState]);
}
i walk through the whole process here https://dev.to/aaronksaunders/expo-router-tab-navigation-from-the-docs-3c38
@josiext if u have an use case where it doesn’t work, showing some code would be helpful?
Have hit the same thing upgrading from Expo-Router V1, and React Navigation before that. I guess the same
useFonts
example that many people were following.This is my current workaround to avoid navigating before the navigator is ready. It also avoids having to add a listener to the navigation state.
Seems to work, until a neater solution is found.
//index.tsx import { useRootNavigationState, Redirect } from 'expo-router'; export default function App() { const rootNavigationState = useRootNavigationState(); if (!rootNavigationState?.key) return null; return <Redirect href={'/test'} /> }
this work perfectly for me thank you !
i've applied it in my index.tsx at the(tabs) folder root :
import {Redirect, useRootNavigationState} from 'expo-router';
const Index = () => {
const rootNavigationState = useRootNavigationState();
if (!rootNavigationState?.key) return null;
return <Redirect href="/actualities" />;
};
export default Index;
here :
@wcastand here is how I a using it
function useProtectedRoute(user) { const segments = useSegments(); const router = useRouter(); const navigationState = useRootNavigationState(); React.useEffect(() => { if (!navigationState?.key) return; const inAuthGroup = segments[0] === "(auth)"; console.log("user", user); if ( // If usernot signed in and the initial segment is not anything in the auth group. !user && !inAuthGroup ) { // Redirect to the login page. router.replace("/login"); } else if (user && inAuthGroup) { // Redirect away from the login page. router.replace("/"); } }, [user, segments, navigationState]); }
i walk through the whole process here https://dev.to/aaronksaunders/expo-router-tab-navigation-from-the-docs-3c38
I just want to note that if you use the useSegments
hook after the useRootNavigationState
hook it will trigger an infinite loop. Is it a bug?
i fixed that too, but i still think it should be handle by expo-router and i shouldn't have to check if it's ready to nav when my hook is already after the isReady check of splashscreen.
If that's the intended behavior then it needs to be in the doc i guess too.
@wcastand Were you maybe able to get rid of The 'redirect' prop on <Screen />...
message?
It was linked to the unstable_settings Once I removed it the warning disappeared
The approaches above didn't work for me but I figured out another way to make it work. It's ugly but works.
import { useRootNavigation, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { Text } from "../components/Themed";
const Index = () => {
const router = useRouter();
const rootNavigation = useRootNavigation();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const checkIfReady = async () => {
const isReady = rootNavigation?.isReady();
if (isReady) {
setIsLoading(false);
router.push("/(tabs)/page");
}
};
checkIfReady();
const intervalId = setInterval(checkIfReady, 1); // Check every second
return () => {
clearInterval(intervalId);
};
}, [rootNavigation]);
return isLoading ? <Text>Loading...</Text> : null;
};
export default Index;
it worked. thanks!
@umit-one i get stuck in an infinite loop of router.pushing :/
This works for me:
// The order is important
const router = useRouter();
const segments = useSegments();
const navigationState = useRootNavigationState();
useEffect(() => {
if (!navigationState?.key) return;
const inAuthGroup = segments[0] === "(auth)";
// This structure may differ from other implementations.
if (user && segments.length === 0) {
router.push("home");
return;
} else if (!user && segments.length === 0) {
router.push("login");
return;
} else if (!user && !inAuthGroup) {
router.push("login");
return;
} else if (user && inAuthGroup) {
router.push("home");
return;
}
}, [user, segments, navigationState]);
@ogmzh the loop might be due to the order of hooks.
@fredrikburmester thanks but i've solved it in another way.. could possibly have been the order of hooks indeed
was this issue closed by mistake? it seems to make sense to let the router handle the root state instead of clients. anyway, two things that might help others:
useRootNavigationState()
you might fix it by moving from app/_layout.tsx
to a inner screen. It worked for me;2.0.2
to 2.0.0
fixed it for me. See #834.@pdandradeb I'm still unclear as to what is causing the redirect prop
warning message. Us using the <Redirect />
component or expo router internals?
I don't understand how this is closed. Documentation is clearly wrong: it mounts protected component and then do redirects. Most of the protected pages rely on authentication context and would simply crash. There are no sensible way to implement redirects for initial URL.
I have also tried to work-around the issue by implementing redirects in auth and main subgroups, but it simply crashes (similar to this: https://github.com/expo/router/issues/510) . This is my structure and redirects:
I've done these two middleware to handle authenticated and not authenticated users, maybe it can help someone:
PS: You need to change the useAuth
to get your own state.
import { router, Slot, useRootNavigation, useSegments } from 'expo-router';
import type { ReactNode } from 'react';
import type React from 'react';
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
interface AuthRoutesProps {
children?: ReactNode;
}
const AuthMiddleware: React.FC<AuthRoutesProps> = () => {
const rootNavigation = useRootNavigation();
const [isNavigationReady, setNavigationReady] = useState(false);
const { user } = useAuth();
const segments = useSegments();
useEffect(() => {
const unsubscribe = rootNavigation?.addListener('state', () => {
// console.log("INFO: rootNavigation?.addListener('state')", event);
setNavigationReady(true);
});
return function cleanup() {
if (unsubscribe) {
unsubscribe();
}
};
}, [rootNavigation]);
useEffect(() => {
if (!user && isNavigationReady) {
router.replace('/(guest)/login'); // redirect to sign-in route
}
}, [user, segments, isNavigationReady]);
return <Slot />;
};
export { AuthMiddleware };
import { router, Slot, useRootNavigation, useSegments } from 'expo-router';
import type { ReactNode } from 'react';
import type React from 'react';
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
interface GuestRoutesProps {
children?: ReactNode;
}
const GuestMiddleware: React.FC<GuestRoutesProps> = () => {
const rootNavigation = useRootNavigation();
const [isNavigationReady, setNavigationReady] = useState(false);
const { user } = useAuth();
const segments = useSegments();
useEffect(() => {
const unsubscribe = rootNavigation?.addListener('state', () => {
// console.log("INFO: rootNavigation?.addListener('state')", event);
setNavigationReady(true);
});
return function cleanup() {
if (unsubscribe) {
unsubscribe();
}
};
}, [rootNavigation]);
useEffect(() => {
if (user && isNavigationReady) {
router.replace('/(guest)/login'); // redirect to sign-in route
}
}, [user, segments, isNavigationReady]);
return <Slot />;
};
export { GuestMiddleware };
Have you fixed this?
@KingsDevs The solution seems to be to scope all signed-in routes under a folder (in @EvanBacon's case called (app)
). Then then inside that folders _layout
redirect to the signed-out layout (auth)
with return <Redirect href="/sign-in" />;
if the user is not signed in.
Has anyone tried this solution?
@smspasha I did, and it seems to be working.
I solved this issue with the following solution:
My _layout that should be protected has a wrapper ProtectedArea component around the navigator, and I prefer using an imperative redirect with a simple setTimeout instead of tracking navigation states. In this case the setTimeout is what makes the solution because it only redirects on the next event loop and thus, giving the root layout the chance to be rendered for the first time.
export default function AppLayout() {
return (
<ProtectedArea>
<Stack />
</ProtectedArea>
);
}
export default function ProtectedArea({ children }: PropsWithChildren) {
const router = useRouter();
const { signedIn, loading } = useAppSelector(selectAuth);
useEffect(() => {
if (!loading && !signedIn) {
setTimeout(() => {
router.replace("/login/");
}, 0);
}
}, [loading, signedIn]);
if (loading || !signedIn) return <BlockUi />; // you could return null, Slot or whatever
return children;
}
For those still stuck on this like I was the docs have been updated with a new pattern for creating authenticated routes with expo router:
https://docs.expo.dev/router/reference/authentication/
Thanks to this PR: https://github.com/expo/router/pull/794
The new pattern is much simpler 🙌
@seanlennaerts I cannot make the solution for official docs to works. I follow this section but still facing the error. https://docs.expo.dev/router/reference/authentication/#navigating-without-navigation expo-router: 2.0.8
I was struggling with this too, I am using Firebase Auth and the Context API to track user status. I couldn't get the documented solution to work and the rootNavigation?.isReady();
check was inconsistent for me. However, the context provider only triggers once and after the Root Layout has mounted so I added a isReady flag to the user context and that was enough!
authContext.tsx
import { createContext } from 'react';
import { User } from 'firebase/auth';
export const AuthContext = createContext<{
user: User | null;
isReady: boolean;
}>({ user: null, isReady: false });
authProvider.tsx
import { useEffect, useState } from 'react';
import { AuthContext } from '../context/authContext';
import { User } from 'firebase/auth';
import { auth } from '../firebaseConfig';
export const AuthProvider = ({ children }: any) => {
const [user, setUser] = useState<{ user: User | null; isReady: boolean }>({
user: null,
isReady: false,
});
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((firebaseUser) => {
setUser({ user: firebaseUser, isReady: true });
});
return unsubscribe;
}, []);
return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>;
};
index.tsx at root
import { useEffect, useContext } from 'react';
import { Link, router } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, Text, Pressable } from 'react-native';
import { AuthContext } from '../context/authContext';
export default function App() {
const { user, isReady: userReady } = useContext(AuthContext);
useEffect(() => {
if (userReady) {
user ? router.replace('/lobby') : router.replace('/login');
}
}, [user]);
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Link href="/login" asChild>
<Pressable style={styles.button}>
<Text style={styles.text}>Login</Text>
</Pressable>
</Link>
<Link href="/register" asChild>
<Pressable style={styles.button}>
<Text style={styles.text}>Sign up</Text>
</Pressable>
</Link>
</View>
);
}
Hope this helps someone!
I was struggling with this too, I am using Firebase Auth and the Context API to track user status. I couldn't get the documented solution to work and the
rootNavigation?.isReady();
check was inconsistent for me. However, the context provider only triggers once and after the Root Layout has mounted so I added a isReady flag to the user context and that was enough!authContext.tsx
import { createContext } from 'react'; import { User } from 'firebase/auth'; export const AuthContext = createContext<{ user: User | null; isReady: boolean; }>({ user: null, isReady: false });
authProvider.tsx
import { useEffect, useState } from 'react'; import { AuthContext } from '../context/authContext'; import { User } from 'firebase/auth'; import { auth } from '../firebaseConfig'; export const AuthProvider = ({ children }: any) => { const [user, setUser] = useState<{ user: User | null; isReady: boolean }>({ user: null, isReady: false, }); useEffect(() => { const unsubscribe = auth.onAuthStateChanged((firebaseUser) => { setUser({ user: firebaseUser, isReady: true }); }); return unsubscribe; }, []); return <AuthContext.Provider value={user}>{children}</AuthContext.Provider>; };
index.tsx at root
import { useEffect, useContext } from 'react'; import { Link, router } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { StyleSheet, View, Text, Pressable } from 'react-native'; import { AuthContext } from '../context/authContext'; export default function App() { const { user, isReady: userReady } = useContext(AuthContext); useEffect(() => { if (userReady) { user ? router.replace('/lobby') : router.replace('/login'); } }, [user]); return ( <View style={styles.container}> <StatusBar style="auto" /> <Link href="/login" asChild> <Pressable style={styles.button}> <Text style={styles.text}>Login</Text> </Pressable> </Link> <Link href="/register" asChild> <Pressable style={styles.button}> <Text style={styles.text}>Sign up</Text> </Pressable> </Link> </View> ); }
Hope this helps someone!
@Smiter15 , Can you show us the file structure? Thanks!!
It's an expo 49 app, using expo-router.
Most of the folders are at root and here is the contents of the root _layout.tsx
import { Slot } from 'expo-router';
import { AuthProvider } from '../provider/authProvider';
export default function Root() {
return (
<AuthProvider>
<Slot />
</AuthProvider>
);
}
@Smiter15 so, the way docs described actually works for you? nice
@Smiter15 so, the way docs described actually works for you? nice
Not exactly. The documentation suggests moving the conditional logic down a level but I have been able to avoid this and keep my logic at the root app level
i think the documentation suggest moving down a level if you want to be able to redirect before showing anything to the user.
like you have a home if connected and a welcome screen otherwise, if you don't want to render index and then have a hook redirect to the right page, you would need to go down one level to achieve that without having the error of trying to render before mounting the root layout component.
at least that's how i understood it.
In your screen, you still have only one access point, maybe you manage the redirect fine and if that's the case then that's good news :)
Which package manager are you using? (Yarn is recommended)
yarn
Summary
I'm using the new docs to implement a basic authentication flow with redirects based on the user's auth status https://docs.expo.dev/router/reference/authentication/. In v1 and sdk 48, this works fine. But in v2 and sdk 49, I'm getting the following error when wrapping my app in an
AuthProvider
. It seems that navigating from within the root layout isn't allowed anymore.Example Repo: https://github.com/andrew-levy/expo-router-v2-repro
AuthProvider
wraps the app in the Root LayoutAuthProvider
callsuseProtectedRoute()
to check the auth status and redirect if necessaryExpo Router: v2 Expo SDK: 49
Minimal reproducible example
https://github.com/andrew-levy/expo-router-v2-repro