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.37k stars 114 forks source link

The 'navigation' object has not been initialized when trying to render <View> in root layout instead of <Slot /> or <Stack> #430

Closed ansh closed 1 year ago

ansh commented 1 year ago

Summary

I am trying to set up some authentication using expo-router. So, I want to show a loading spinner while the auth data is fetching (ideally I wouldn't need this and could just keep the SplashScreen up but that's another story completely).

Anyway, this is my root layout

import "react-native-gesture-handler"; // must be imported in root layout
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { useFonts } from "expo-font";
import { Slot, SplashScreen, Stack, useFocusEffect, usePathname, useRouter, useSegments } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, useColorScheme } from "react-native";
import { QueryClientProvider } from "@tanstack/react-query";
import { TRPC_BASE_URL, queryClient, trpcClient, trpcQuery } from "../config/api";
import { AuthProvider, useAuth } from "../config/auth";
import { View } from "../components/Themed";

export {
  // Catch any errors thrown by the Layout component.
  ErrorBoundary,
} from "expo-router";

export const unstable_settings = {
  // Ensure that reloading on `/modal` keeps a back button present.
  initialRouteName: "(tabs)",
};

export default function RootLayout() {
  const [loaded, error] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
    ...FontAwesome.font,
  });

  // Expo Router uses Error Boundaries to catch errors in the navigation tree.
  useEffect(() => {
    if (error) throw error;
  }, [error]);

  return (
    <>
      {/* Keep the splash screen open until the assets have loaded. In the future, we should just support async font loading with a native version of font-display. */}
      {!loaded && <SplashScreen />}
      {loaded && <AppRoot />}
    </>
  );
}

function AppRoot() {
  return (
    <trpcQuery.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <AuthProvider>
          <RootLayoutNav />
        </AuthProvider>
      </QueryClientProvider>
    </trpcQuery.Provider>
  );
}

function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const { isLoading } = useAuth();

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="modal" options={{ presentation: "modal" }} />
      </Stack>
    </ThemeProvider>
  );
}

For some reason, the code in the RootLayoutNav component that is shown below leads to the error The 'navigation' object hasn't been initialized yet. This might happen if you don't have a navigator mounted, or if the navigator hasn't finished mounting. See https://reactnavigation.org/docs/navigating-without-navigation-prop#handling-initialization for more details.

  if (isLoading) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

Not sure why this is happening. I assume it is because the root layout expects to render a Stack or a Slot or something, but not too sure if I'm doing something wrong or if there is a bug within expo-router

Minimal reproducible example

I added some code above but I can make a repo if necessary.

EvanBacon commented 1 year ago

There's a lot going on here so it's hard to say what the React Navigation-y workaround is. The long-term solution is to rewrite more parts of React Navigation so it doesn't need to render navigators in order to handle an event. This is exclusive to Expo Router since we always know how to handle routing ahead of time.

ansh commented 1 year ago

@EvanBacon I have simplified it and made a reproduction here: https://github.com/ansh/expo-router-auth-issue-repro

You can yarn and yarn start to check it out (Expo Go compatible reproduction). I am not using any auth library but instead just mocking some API endpoint for this reproduction.

As you can see, I have followed the expo-router docs on Authentication almost exactly, except I have the isLoading if statement within my RootLayoutNav that renders a loading screen while data is fetching. This is a better user experience than showing the user the wrong screen. This was quite trivial to do with React Navigation but trying to figure out how to do this with expo-router has been quite difficult.

ansh commented 1 year ago

@EvanBacon Any updates on this? Happy to help if I can. Just point me to the correct direction.

marklawlor commented 1 year ago

This should be fixed on main and will be included in the next version.

ansh commented 1 year ago

Thanks so much! Can you link to the PR it was fixed in? @marklawlor

marklawlor commented 1 year ago

There isn't a direct PR for this issue, just general refactoring. But we are now delay rendering the screens until the navigator is ready.

https://github.com/expo/router/blob/e3fbf9ad5eb4fa41b1d2b80b9b4bd97cc25be969/packages/expo-router/src/ExpoRoot.tsx#L169

edmbn commented 1 year ago

I'm using 1.7.2 and having then same issue, should this version have this solved? If I render <Slot /> conditionally after needed data is loaded always throws: Error: Got 'undefined' for the navigation state. You must pass a valid state object.

Albert-Gao commented 1 year ago

For people who have this error, rule of thumb,

you MUST pass the navigation children down.

For example, I am using react native firebase, where in its doc, it suggests you to wait for the initialization to finish then render the UI, somethin like this:

if (!isInit) {
   return <Text>Loading...</Text>
}

I put the above logic within a component at the top-level _layout.tsx file, and got this error, the reason is, this return prevents the children to be passed, instead, it renders this <Text> component, once I removed the above logic, no error at all.

dont worry, you can always find some layer to a logic like this :)

marklawlor commented 1 year ago

If you are still experiencing this issue with Expo Router v2 can you please provide a reproduction.

caioedut commented 1 year ago

My workaround:

import { useNavigation } from "expo-router";

const navigation = useNavigation();

Using conditional with: navigation.isReady() ``

gaetan224 commented 1 year ago

still having this issue on v2


function RootLayoutNav() {
  const colorScheme = useColorScheme();
  const [firstLaunch, setFirstLaunch] = useState<boolean | null>(null);
  const navigation = useNavigation();
  const [isReady, setIsReady] = useState(false)

  navigation.addListener('state', () => {
    setIsReady(true)
  });

  useEffect(() => {
    AsyncStorage.removeItem(APP_LAUNCHED_KEY);
    AsyncStorage.getItem(APP_LAUNCHED_KEY).then((value) => {
      console.log(value);
      if (value === null) {
        AsyncStorage.setItem(APP_LAUNCHED_KEY, "false");
        setFirstLaunch(true);
      } else {
        setFirstLaunch(false);
      }
    });
  }, []);

  if (firstLaunch && isReady) {
    return <Redirect href="onboarding"/>;
  } else {
    return (
        <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
          <Stack>
            <Stack.Screen name="(tabs)" options={{headerShown: false}}/>
            <Stack.Screen name="modal" options={{presentation: 'modal'}}/>
          </Stack>
        </ThemeProvider>
    );
  }
}```
fredrikburmester commented 1 year ago

I'm also having this issue on V2.

zataara commented 1 year ago

I'm having this issue as well with V2, any conditional rendering causes the title error.

import useTheme from "../hooks/useTheme";
import { View } from "react-native";
import useAuth from "../hooks/useAuth";
import { Redirect } from "expo-router";

interface PublicLayoutProps {
  children: ReactNode;
}

const PublicLayout = ({ children }: PublicLayoutProps) => {
  const { theme } = useTheme();

  const { currentUser } = useAuth();

  if (currentUser && currentUser.isVerified) {
    return <Redirect href="/private" />;
  } else if (currentUser && !currentUser.isVerified) {
    return <Redirect href="/unverified" />;
  }

  return (
    <View style={{ backgroundColor: theme.background, height: "100%" }}>
      {children}
    </View>
  );
};

export default PublicLayout;
sannajammeh commented 1 year ago

I've been able to patch the first issue using useRootNavigation, now there's the problem of actually redirecting before we have a slot available see:

New error:

ERROR  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.
function Root() {
  const [onboarded, setOnboarded] = useMMKVBoolean("app.onboarded");
  const navigation = useRootNavigation();
  // const router = useRouter();

  if (!onboarded && navigation?.isReady())
    return <Redirect href="/onboarding/" />;
  return (
    <>
      <Modals />
      <Stack
        screenOptions={{
          headerShown: false,
          headerTitleStyle: {
            fontFamily: "Poppins_SemiBold",
            fontSize: 18,
          },
        }}
      >
        <Stack.Screen name="(tabs)" />
        <Stack.Screen
          name="payment-modal"
          options={{
            headerShown: true,
            presentation: "modal",
          }}
        />
      </Stack>
    </>
  );
}
Noitham commented 1 year ago

I've been able to patch the first issue using useRootNavigation, now there's the problem of actually redirecting before we have a slot available see:

New error:

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

If I'm not wrong, on expo-router we should manage the auth state via a context, at least, that's what's recommended:

https://docs.expo.dev/router/reference/authentication/

sannajammeh commented 1 year ago

I've been able to patch the first issue using useRootNavigation, now there's the problem of actually redirecting before we have a slot available see: New error:

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

If I'm not wrong, on expo-router we should manage the auth state via a context, at least, that's what's recommended:

https://docs.expo.dev/router/reference/authentication/

Yeah thats what makes sense and is also what I'm doing. The problem is the lack of documentation on edge cases of expo router. There's no explanation of these navigation errors, no explanation of the weird behaviour of unstable_settings and no explanation of internal API's which are used in some of the examples.

Its documented for simple use, thats about it.

drweizak commented 1 year ago

Any news on this?

ansh commented 1 year ago

For those wondering. This is the new Authentication reference docs for Expo Router. https://docs.expo.dev/router/reference/authentication/

They are quite good!

derek-primumco commented 1 year ago

Anyone find a working solution for handling root-level redirect logic in Expo Router v2? The new docs mention the problem, but not how to solve the issue without nesting layouts.

Thanks @caioedut for a solution that worked in Expo Router v1...

My workaround:

import { useNavigation } from "expo-router";

const navigation = useNavigation();

Using conditional with: navigation.isReady() ``

But that's been removed now in Expo Router v2.

--

edit: I was able to get this type of root layout router.push() working without problems by replacing router = useRouter() (using Solito's cross-platform router) to import { router } from "expo-router".