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

TheLukaDragar commented 11 months ago

I solved it by adding

const [isNavigationReady, setNavigationReady] = useState(false);
const rootNavigation = useRootNavigation();
useEffect(() => {
  const unsubscribe = rootNavigation?.addListener("state", (event) => {
    setNavigationReady(true);
  });
  return function cleanup() {
    if (unsubscribe) {
      unsubscribe();
    }
  };
}, [rootNavigation]);

React.useEffect(() => {
  if (!isNavigationReady) {
    return;
  }

  const inAuthGroup = segments[0] === "(auth)";

  if (!authInitialized) return;

  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.push("/sign-in");
  } else if (user && inAuthGroup) {
    // Redirect away from the sign-in page.
    router.push("/");
  }
}, [user, segments, authInitialized, isNavigationReady]);

as detailed https://github.com/aaronksaunders/expo-router-v2-authflow-appwrite/blob/main/app/context/auth.tsx

danuja01 commented 11 months 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 />;
}

After hours of trying, It worked! Thanks alot!

VictorCiechovicz commented 9 months ago

my solution is monitoring the navigationState.key then after this i apply the redirect

const navigationState = useRootNavigationState()

useEffect(() => { if (navigationState.key) { user.displayName ? router.replace('/index') : router.replace('/changeDisplayName') } }, [navigationState, user])

coyoteazul commented 8 months ago

a note for anyone finding this issue. The problem mentions root, but the problem won't necesarily be there

I had to change this function export default function Cliente () { const factura_context = useFacturaContext() if (factura_context?.state) { return ( ...render my stuff ) } else { router.push("/(main)/facturador"); return (<></>) } };

and replace else with this } else { return (<Redirect href={"/(main)/facturador"}/>) }

cornejobarraza commented 8 months ago

I was having this issue again because I was returning a loading component in my context provider, I removed it and the error stopped showing up. I had already implemented the useRootNavigation workaround in my protected route hook before:

export function MyProvider(props) {
  const [isLoading, setIsLoading] = useState(true);

  // This is where I redirect
  useProtectedRoute();

  // This is what I removed
  if (isLoading) {
    return <Text>Loading...</Text>
  }

  return (
    <MyContext.Provider
      value={{
        ...
      }}
    >
      {props.children}
    </MyContext.Provider>
  );
}
KartikBazzad commented 8 months ago

create a hook:

import { useEffect, useState } from "react";
import { useRouter, useNavigationContainerRef } from "expo-router";
export default function useExtendedRouter() {
  const [isNavigationReady, setIsNavigationReady] = useState(false);
  const router = useRouter();
  const navigationContainerRef = useNavigationContainerRef();
  useEffect(() => {
    const sub = navigationContainerRef.addListener("state", () => {
      setIsNavigationReady(true);
    });
    return () => {
      if (sub) {
        sub();
      }
    };
  }, [navigationContainerRef]);
  return { isReady: isNavigationReady, ...router };
}

inside the component


const router = useExtendedRouter();

if(router.isReady){
<Redirect to="/home" />
}

 useEffect(() => {
    if (isAuthorized && router.isReady) {
      router.replace('/home');
    }
  }, [ isAuthorized, router]);
mohit4bug commented 7 months ago

This worked for me.

import { FullScreenLoader } from "components/full-screen-loader"
import { useRootNavigationState, useRouter, useSegments } from "expo-router"
import * as secureStore from "expo-secure-store"
import { createContext, useContext, useEffect, useState } from "react"

interface AuthContextProps {
    signOut: () => Promise<void>
}

const AuthContext = createContext({} as AuthContextProps)

export const useAuth = () => useContext(AuthContext)

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
    const router = useRouter()
    const segments = useSegments()
    const navigationState = useRootNavigationState()
    const [isLoading, setIsLoading] = useState(true)

    const signOut = async () => {
        await secureStore.deleteItemAsync("JWTTOKEN")
        router.replace("/(auth)/login")
    }

    useEffect(() => {
        setIsLoading(false)
        if (!navigationState?.key) return

        const isAuthGroup = segments[0] === "(auth)"

        const jwt = secureStore.getItem("JWTTOKEN")

        if (!jwt && !isAuthGroup) {
            router.replace("/(auth)/login")
        } else if (jwt && isAuthGroup) {
            router.replace("/(authenticated)/dashboard")
        }
    }, [segments])

    return (
        <AuthContext.Provider value={{ signOut }}>
            {isLoading ? <FullScreenLoader /> : children}
        </AuthContext.Provider>
    )
}
mleister97 commented 7 months ago
import { useRootNavigationState } from 'expo-router'

export function useIsNavigationReady() {
  const rootNavigationState = useRootNavigationState()
  return rootNavigationState?.key != null
}
const isNavigationReady = useIsNavigationReady()
// Do not perform any routing operations as long as this variable is set to false.
SaM-0777 commented 6 months 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 <#740 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEAFGMQUPAHGFKDN2E2QNTXQ2A4RANCNFSM6AAAAAA2ARJYTE . You are receiving this because you were mentioned.Message ID: @.***>

Because It should already be in Ready state... Thats why

pedromartinsjesus commented 3 months ago

It worked for me Instead a useEffect() inside index.tsx i use it inside _layout.tsx like this

//_layout.tsx

import { Stack, useRouter } from "expo-router"; import { useEffect,useState } from "react";

export default function RootLayout() {

const [logged,setLogged] = useState(true); const router = useRouter();

useEffect(()=>{ checkLogged(); },[]);

const checkLogged = ()=>{ if(logged) router.push("/group_1/inside"); }

return (

); }

rickbhatt commented 3 months ago

For redirection purposes, have you ever tried using ? I tried using that to counter this problem and this is something that worked for. I do not know if this is a correct way to redirect users after authentication, but with this I do not have to check for the ready state.

snswap828 commented 3 weeks ago

const rootNavigationState = useRootNavigationState(); useEffect(() => { if(rootNavigationState?.routeNames?.includes('home')) router.replace('home'); },[rootNavigationState])

This work for me

amloelxer commented 6 days ago

For those trying to programmatically route with the router in expo router v3, using the the redirect component solved the issue for me.

In my app/(app)/_layout.tsx file

const AppLayout = () => {
    const currentUser = contextObject?.currentUser;
    if (isLoading) {
      return <LoadingScreen />;
    }

    if(!currentUser) {
      // BAD  
      // router.replace('/SignIn')

      // Fixes the issue
      return <Redirect href="/SignIn" />;
    }

    return <Stack />
}