expo / router

[ARCHIVE]: Expo Router has moved to expo/expo -- The File-based router for universal React Native apps
https://docs.expo.dev/routing/introduction/
1.36k stars 113 forks source link

Attempted to navigate before mounting the Root Layout component #740

Closed andrew-levy closed 1 year ago

andrew-levy commented 1 year ago

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

Expo Router: v2 Expo SDK: 49

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.
This error is located at:
    in AuthProvider (created by RootLayout)
    in RootLayout
    in Unknown (created by Route())
    in Route (created by Route())
    in Route() (created by ContextNavigator)
    in RNCSafeAreaProvider (created by SafeAreaProvider)
    in SafeAreaProvider (created by wrapper)
    in RCTView (created by View)
    in View (created by GestureHandlerRootView)
    in GestureHandlerRootView (created by GestureHandlerRootView)
    in GestureHandlerRootView (created by wrapper)
    in wrapper (created by ContextNavigator)
    in EnsureSingleNavigator
    in BaseNavigationContainer
    in ThemeProvider
    in NavigationContainerInner (created by ContextNavigator)
    in ContextNavigator (created by ExpoRoot)
    in ExpoRoot (created by App)
    in App (created by withDevTools(App))
    in withDevTools(App) (created by ErrorOverlay)
    in ErrorToastContainer (created by ErrorOverlay)
    in ErrorOverlay
    in RCTView (created by View)
    in View (created by AppContainer)
    in RCTView (created by View)
    in View (created by AppContainer)
    in AppContainer
    in main(RootComponent), js engine: hermes

Minimal reproducible example

https://github.com/andrew-levy/expo-router-v2-repro

henryjperez commented 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 />;
}
fluid-design-io commented 1 year ago

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

GoldMyr1994 commented 1 year ago

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
 }
}
fmendez89 commented 1 year ago

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.

sbkl commented 1 year ago

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.

GoldMyr1994 commented 1 year ago

@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

Screenshot 2023-07-07 alle 13 20 10

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>
zRelux commented 1 year ago

Same issue

mr-cactus commented 1 year ago

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'} />
}
Scr3nt commented 1 year ago

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) image

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

Dundyne commented 1 year ago

Same issue, tried @mr-cactus solution and it works

fmendez89 commented 1 year ago

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.

jdziadek commented 1 year ago

I have the same problem, I used setTimeout to redirect to the correct screen. Now it works, but it's not an elegant solution.

Scr3nt commented 1 year ago

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

aaronksaunders commented 1 year ago

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.

Scr3nt commented 1 year ago

@aaronksaunders Sure, it works, but in my case it adds 1.5 seconds to my startup time.

aaronksaunders commented 1 year ago

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.

aaronksaunders commented 1 year ago

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: @.***>

miran248 commented 1 year ago

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",
};
chmerev commented 1 year ago

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; }

wcastand commented 1 year ago

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.

Screenshot 2023-07-25 at 12 20 37

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
aaronksaunders commented 1 year ago

@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

aaronksaunders commented 1 year ago

@josiext if u have an use case where it doesn’t work, showing some code would be helpful?

st3rbenn commented 1 year ago

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 :

Capture d’écran 2023-08-01 à 12 50 48

ayloncarrijo commented 1 year ago

@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?

wcastand commented 1 year ago

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.

mb8z commented 1 year ago

@wcastand Were you maybe able to get rid of The 'redirect' prop on <Screen />... message?

wcastand commented 1 year ago

It was linked to the unstable_settings Once I removed it the warning disappeared

umit-one commented 1 year ago

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;
jintu-das commented 1 year ago

it worked. thanks!

ogmzh commented 1 year ago

@umit-one i get stuck in an infinite loop of router.pushing :/

fredrikburmester commented 1 year ago

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.

ogmzh commented 1 year ago

@fredrikburmester thanks but i've solved it in another way.. could possibly have been the order of hooks indeed

pdandradeb commented 1 year ago

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:

ogmzh commented 1 year ago

@pdandradeb I'm still unclear as to what is causing the redirect prop warning message. Us using the <Redirect /> component or expo router internals?

ex3ndr commented 1 year ago

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.

ex3ndr commented 1 year ago

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:

image image image
joaomantovani commented 1 year ago

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.

AuthMiddleware.tsx

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 };

GuestMiddleware.tsx

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 };
KingsDevs commented 1 year ago

Have you fixed this?

fredrikburmester commented 1 year ago

@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.

smspasha commented 1 year ago

Has anyone tried this solution?

fredrikburmester commented 1 year ago

@smspasha I did, and it seems to be working.

diego-rangel commented 1 year ago

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;
}
seanlennaerts commented 12 months ago

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 🙌

xquangdang commented 11 months ago

@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

Smiter15 commented 11 months ago

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!

joaku commented 11 months ago

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!!

Smiter15 commented 11 months ago

It's an expo 49 app, using expo-router.

Screenshot 2023-10-10 at 08 50 42

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>
  );
}
ogmzh commented 11 months ago

@Smiter15 so, the way docs described actually works for you? nice

Smiter15 commented 11 months ago

@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

wcastand commented 11 months ago

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 :)