Skip to content

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.

src/app/_layout.tsx
// Import global CSS file
import '../../global.css';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import React from 'react';
import { 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 { APIProvider } from '@/api';
import { hydrateAuth, loadSelectedTheme } from '@/lib';
import { useThemeConfig } from '@/lib/use-theme-config';
export { ErrorBoundary } from 'expo-router';
export const unstable_settings = {
initialRouteName: '(app)',
};
hydrateAuth();
loadSelectedTheme();
// 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() {
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>
);
}
function Providers({ children }: { children: React.ReactNode }) {
const theme = useThemeConfig();
return (
<GestureHandlerRootView
style={styles.container}
className={theme.dark ? `dark` : undefined}
>
<KeyboardProvider>
<ThemeProvider value={theme}>
<APIProvider>
<BottomSheetModalProvider>
{children}
<FlashMessage position="top" />
</BottomSheetModalProvider>
</APIProvider>
</ThemeProvider>
</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.

src/app/(app)/_layout.tsx
/* eslint-disable react/no-unstable-nested-components */
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="px-3 text-primary-300">Create</Text>
</Pressable>
</Link>
);
};

Make sure to check the official docs for more information and examples about expo-router.