Typescript
react native
Expo Router
Advanced Navigation
Mobile UX
These patterns are what separate a functional app from a professional app. And while they may seem intimidating at first, once you understand the logic behind each one, they become powerful tools for building sophisticated experiences.
1app/ 2 ├─ (tabs)/ 3 │ ├─ _layout.tsx → Tab Navigator 4 │ ├─ index.tsx → "Home" Tab 5 │ ├─ cart.tsx → "Cart" Tab 6 │ └─ profile/ 7 │ ├─ _layout.tsx → Internal Stack Navigator 8 │ ├─ index.tsx → Profile main screen 9 │ ├─ edit.tsx → Edit profile 10 │ └─ history.tsx → Order history 11 └─ _layout.tsx → Root layout
In this case, the "Profile" tab is not just a screen: it's a full stack. When the user enters their profile, they can navigate to edit data or view history, and when they go back, they return to the main profile screen, not the previous tab.
1// app/(tabs)/_layout.tsx 2import { Tabs } from "expo-router"; 3import { Home, ShoppingCart, User } from "lucide-react-native"; 4 5export default function TabsLayout() { 6 return ( 7 <Tabs> 8 <Tabs.Screen 9 name="index" 10 options={{ 11 title: "Home", 12 tabBarIcon: ({ color }) => <Home color={color} size={24} /> 13 }} 14 /> 15 <Tabs.Screen 16 name="cart" 17 options={{ 18 title: "Cart", 19 tabBarIcon: ({ color }) => <ShoppingCart color={color} size={24} /> 20 }} 21 /> 22 <Tabs.Screen 23 name="profile" 24 options={{ 25 title: "Profile", 26 tabBarIcon: ({ color }) => <User color={color} size={24} /> 27 }} 28 /> 29 </Tabs> 30 ); 31}
Internal Profile Stack:
1// app/(tabs)/profile/_layout.tsx 2import { Stack } from "expo-router"; 3 4export default function ProfileLayout() { 5 return ( 6 <Stack> 7 <Stack.Screen 8 name="index" 9 options={{ headerShown: false }} // Tab already shows header 10 /> 11 <Stack.Screen 12 name="edit" 13 options={{ title: "Edit Profile" }} 14 /> 15 <Stack.Screen 16 name="history" 17 options={{ title: "Order History" }} 18 /> 19 </Stack> 20 ); 21}
1// app/(tabs)/profile/index.tsx 2import { Link } from "expo-router"; 3 4export default function ProfileScreen() { 5 return ( 6 <View> 7 <Text>Welcome to your profile</Text> 8 <Link href="/profile/edit">Edit Data</Link> 9 <Link href="/profile/history">View History</Link> 10 </View> 11 ); 12}
A screen's header is not just aesthetic: it communicates context, offers quick actions, and reinforces the app's identity. By default, Expo Router gives you a basic header, but professional apps need more control.
You can modify colors, fonts, and header elements from each screen's options:
1<Stack.Screen 2 name="details" 3 options={{ 4 title: "Product Details", 5 headerStyle: { backgroundColor: "#6200ee" }, 6 headerTintColor: "#fff", 7 headerTitleStyle: { fontWeight: "bold" } 8 }} 9/>
This works for simple cases. But what if you need a fully custom header?
1<Stack.Screen 2 name="product" 3 options={{ 4 headerTitle: () => <CustomHeader />, 5 headerRight: () => ( 6 <TouchableOpacity onPress={() => console.log("Share")}> 7 <Share2 color="#fff" size={24} /> 8 </TouchableOpacity> 9 ) 10 }} 11/>
You can insert any React component into the header. This opens up endless possibilities: search bars in the header, filter buttons, loading indicators, animations.
A very common pattern in modern apps is the header that hides when scrolling down and reappears when scrolling up. This maximizes content space without sacrificing navigation.
1<Stack.Screen 2 name="feed" 3 options={{ 4 headerTransparent: true, 5 headerBlurEffect: "light" 6 }} 7/>
Or even better, use Animated
to create smooth transitions based on user scroll. While this requires a bit more code, the result is a fluid, professional experience.
Deep linking allows users to open your app on a specific screen from an external link: an email, a push notification, a QR code. It's essential for marketing, retention, and user experience.
With Expo Router, deep linking works automatically thanks to the file-based structure. You just need to configure the URL scheme:
1// app.json 2{ 3 "expo": { 4 "scheme": "myapp" 5 } 6}
Now, a link like myapp://profile/42
will open the profile screen with userId: 42
.
For real URLs (https://yourapp.com/profile/42
), you need to configure universal links and app links. This requires verification files on your domain, but Expo simplifies the process with EAS:
1{ 2 "expo": { 3 "ios": { 4 "associatedDomains": ["applinks:yourapp.com"] 5 }, 6 "android": { 7 "intentFilters": [ 8 { 9 "action": "VIEW", 10 "data": { "scheme": "https", "host": "yourapp.com" } 11 } 12 ] 13 } 14 } 15}
Now, when someone clicks a link from your domain, the OS will ask if they want to open the app. If the user accepts, the app opens directly on the correct screen.
1<Stack.Screen 2 name="details" 3 options={{ 4 animation: "fade", 5 // or "slide_from_bottom", "slide_from_right", "flip", etc. 6 }} 7/>
1cardStyleInterpolator: ({ current, layouts }) => ({ 2 cardStyle: { 3 transform: [ 4 { 5 translateX: current.progress.interpolate({ 6 inputRange: [0, 1], 7 outputRange: [layouts.screen.width, 0], 8 }), 9 }, 10 ], 11 }, 12})
This requires understanding interpolation and animations, but the result is total control over how screens move. Some premium apps use custom transitions to reinforce their visual identity.