Expo Router
expo-router is a navigation library provided by Expo that simplifies the implementation of navigation in React Native applications. It is built on top of React Navigation, a widely used navigation library, and abstracts away much of the complexity involved in managing navigation state and transitions between screens.
Navigation in Expo Router is expressed declaratively, utilizing components to define the flow of the application. This approach makes it intuitive for developers to structure their navigation hierarchy.
Conventional React Native projects typically adopt a structure where a sole root component is commonly specified in either ./App.js or ./index.js. Within the context of Expo Router, an alternative approach is offered through the utilization of the Root Layout, located in app/_layout.tsx in our Demo. Thereby, the _layout section of our app handles the overall structure and navigation setup.
// Import global CSS fileimport '../../global.css';
import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, useFonts,} from '@expo-google-fonts/inter';import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';import { Stack } from 'expo-router';import * as SplashScreen from 'expo-splash-screen';import { HeroUINativeProvider } from 'heroui-native';import React, { useCallback } from 'react';import { KeyboardAvoidingView, StyleSheet } from 'react-native';import FlashMessage from 'react-native-flash-message';import { GestureHandlerRootView } from 'react-native-gesture-handler';import { KeyboardProvider } from 'react-native-keyboard-controller';import { configureReanimatedLogger, ReanimatedLogLevel,} from 'react-native-reanimated';
import { APIProvider } from '@/api';import { hydrateAuth } from '@/lib';import { AppThemeProvider } from '@/lib/contexts/app-theme-context';
export { ErrorBoundary } from 'expo-router';
configureReanimatedLogger({ level: ReanimatedLogLevel.warn, strict: false,});
export const unstable_settings = { initialRouteName: '(app)',};
hydrateAuth();// Prevent the splash screen from auto-hiding before asset loading is complete.SplashScreen.preventAutoHideAsync();// Set the animation options. This is optional.SplashScreen.setOptions({ duration: 500, fade: true,});
export default function RootLayout() { const fonts = useFonts({ Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, });
if (!fonts) { return null; }
return ( <Providers> <Stack> <Stack.Screen name="(app)" options={{ headerShown: false }} /> <Stack.Screen name="onboarding" options={{ headerShown: false }} /> <Stack.Screen name="login" options={{ headerShown: false }} /> </Stack> </Providers> );}
const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => { const contentWrapper = useCallback( (children: React.ReactNode) => ( <KeyboardAvoidingView pointerEvents="box-none" behavior="padding" keyboardVerticalOffset={12} className="flex-1" > {children} </KeyboardAvoidingView> ), [] );
return ( <GestureHandlerRootView style={styles.container}> <KeyboardProvider> <AppThemeProvider> <HeroUINativeProvider config={{ toast: { contentWrapper, }, }} > <APIProvider> <BottomSheetModalProvider> {children} <FlashMessage position="top" /> </BottomSheetModalProvider> </APIProvider> </HeroUINativeProvider> </AppThemeProvider> </KeyboardProvider> </GestureHandlerRootView> );};
const styles = StyleSheet.create({ container: { flex: 1, },});The Demo app comes with a simple stack and tabs layout. Feel free to remove what is not working for you and add your own using the same approach as the existing ones.
Here is a simple example of the tabs layout.
import { Link, Redirect, SplashScreen, Tabs } from 'expo-router';import React, { useCallback, useEffect } from 'react';
import { Pressable, Text } from '@/components/ui';import { Feed as FeedIcon, Settings as SettingsIcon, Style as StyleIcon,} from '@/components/ui/icons';import { useAuth, useIsFirstTime } from '@/lib';
export default function TabLayout() { const status = useAuth.use.status(); const [isFirstTime] = useIsFirstTime(); const hideSplash = useCallback(async () => { await SplashScreen.hideAsync(); }, []); useEffect(() => { if (status !== 'idle') { setTimeout(() => { hideSplash(); }, 1000); } }, [hideSplash, status]);
if (isFirstTime) { return <Redirect href="/onboarding" />; } if (status === 'signOut') { return <Redirect href="/login" />; } return ( <Tabs> <Tabs.Screen name="index" options={{ title: 'Feed', tabBarIcon: ({ color }) => <FeedIcon color={color} />, headerRight: () => <CreateNewPostLink />, tabBarButtonTestID: 'feed-tab', }} />
<Tabs.Screen name="style" options={{ title: 'Style', headerShown: false, tabBarIcon: ({ color }) => <StyleIcon color={color} />, tabBarButtonTestID: 'style-tab', }} /> <Tabs.Screen name="settings" options={{ title: 'Settings', headerShown: false, tabBarIcon: ({ color }) => <SettingsIcon color={color} />, tabBarButtonTestID: 'settings-tab', }} /> </Tabs> );}
const CreateNewPostLink = () => { return ( <Link href="/feed/add-post" asChild> <Pressable> <Text className="text-primary-300 px-3">Create</Text> </Pressable> </Link> );};Make sure to check the official docs for more information and examples about expo-router.