Expo Router
Expo Router werkt zeer gelijkaardig aan de routing in next.js. Het is een file-based router. Dit wil zeggen dat je een mapstructuur aanmaakt en dat je op basis van de bestandsnaam de route bepaalt. Het laat toe om navigatie tussen schermen in je app te beheren, waardoor gebruikers naadloos tussen verschillende delen van de UI van je app kunnen bewegen, met behulp van dezelfde componenten op meerdere platforms (Android, iOS en web).
Het is gebouwd op React Navigation, een populaire library voor het beheren van navigatie in React Native apps.
Installatie
Je hoeft Expo Router niet apart te installeren. Een nieuw project dat je aanmaakt met npx create-expo-app@latest gebruikt standaard al Expo Router. Kies bij de vraag naar de SDK-versie altijd Latest en verwijder daarna de voorbeeldcode met npm run reset-project (antwoord n). Zie Nieuwe app aanmaken.
Schermen toevoegen
Als een bestand wordt aangemaakt in de app directory (in een nieuw project is dat src/app), wordt het automatisch een route in de app. In de voorbeelden hieronder schrijven we kortweg app/.... Bijvoorbeeld, de volgende bestanden zullen de volgende routes maken:
app/index.tsxkomt overeen met/app/home.tsxkomt overeen met/homeapp/settings/index.tsxkomt overeen met/settingsapp/[user].tsxkomt overeen met dynamische paden zoals/expoof/joske
Deze paden kunnen worden gebruikt in de Link component om te navigeren naar een bepaald scherm. Als je de applicatie op een webbrowser opent, kan je ook de URL aanpassen om naar een bepaald scherm te navigeren.
Het eerste scherm
In een nieuw project staan de schermen in de map src/app. Na npm run reset-project vind je daar het bestand index.tsx. Dit bestand wordt gebruikt als eerste scherm van je applicatie, op de route /.
import React from "react";
import { View, StyleSheet, Text } from "react-native";
const Index = () => {
return (
<View style={styles.container}>
<Text>Home Screen</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
})
export default Index;
Als je de applicatie opstart met npx expo start, dan zie je dit scherm.
Nog een scherm toevoegen
Willen we een tweede scherm toevoegen voor een bepaalde route dan maken we een bestand aan in de app directory met de naam van de route. Bijvoorbeeld app/about.tsx voor de route /about.
import React from "react";
import { View, StyleSheet, Text } from "react-native";
const About = () => {
return (
<View style={styles.container}>
<Text>About Screen</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
})
export default About;
Dynamische routes
Je kan ook dynamische routes maken. Dit doe je door een bestand aan te maken met de naam van de route en tussen vierkante haken de naam van de parameter. Bijvoorbeeld app/users/[name].tsx voor de route /users/expo of /users/joske.
import { useLocalSearchParams } from "expo-router";
import React from "react";
import { View, StyleSheet, Text } from "react-native";
const User = () => {
const { name } = useLocalSearchParams<{name: string}>();
return (
<View style={styles.container}>
<Text>User : { name }</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
})
export default User;
Navigeren tussen schermen
Expo Router gebruikt het Link component om te navigeren tussen schermen. Dit component heeft een href property die de route bepaalt.
<Link href="/about">About</Link>
Het Link component wrapt de children in een <Text> component. Dit is handig voor accessibility maar niet altijd gewenst. Je kan het component aanpassen door de asChild property te gebruiken. Deze property zal alle props doorgeven aan het eerste child van het Link component. Het child component moet de onPress en onClick props ondersteunen.
<Link href="/about" asChild>
<Button title="About" />
</Link>
of gebruik makende van de Pressable component:
<Link href="/about" asChild>
<Pressable>
<Text>About</Text>
</Pressable>
</Link>
Imperatief navigeren
Je kan ook imperatief (via programma code) navigeren. Bijvoorbeeld als je een formulier hebt en je wil na het indienen van het formulier naar een andere pagina navigeren.
import { router } from 'expo-router';
...
router.replace("/about");
...
Op het router object kan je de volgende methodes gebruiken:
push: voegt een nieuwe pagina toe aan de navigatie stackreplace: vervangt de huidige pagina in de navigatie stackback: gaat terug naar de vorige pagina in de navigatie stackcanGoBack: geeft aan of er een vorige pagina is in de navigatie stacksetParams: verandert de parameters van de huidige pagina in de navigatie stack
Dynamische routes linken
Je kan dynamische router linken door zelf de route aan te maken door concatenatie van de route en de parameters. Of je kan hiervoor het Href object gebruiken.
<Link
href={{
pathname: "/users/[name]",
params: { name: 'bacon' }
}}>
View user
</Link>
Route vervangen
Per default worden routes toegevoegd aan de navigatie stack. Dit betekent dat de vorige pagina beschikbaar is als de gebruiker terug navigeert. Je kan de replace property gebruiken om de huidige pagina te vervangen in plaats van een nieuwe toe te voegen.
<Link href="/about" replace>
<Text>About</Text>
</Link>
Layout routes
Normaal gezien vult een route het volledige scherm. Als je tussen schermen navigeert is dit een volledige pagina transitie zonder animatie. In native apps verwachten gebruikers dat gedeelde elementen zoals headers en tab bars behouden blijven tussen pagina's. Deze worden gemaakt met behulp van layout routes.
Layout route aanmaken
Om een layout route aan te maken maak je een bestand aan met de naam _layout.tsx in de app directory.
import { Slot } from 'expo-router';
const HomeLayout = () => {
return <Slot />;
}
export default HomeLayout;
In het voorbeeld hierboven zal de Slot component de huidige child route renderen. Dit is vergelijkbaar met de children prop in React. Deze component kan gewrapt worden met andere componenten om een layout te maken.
import { Slot } from 'expo-router';
const HomeLayout = () => {
return (
<View>
<Text>Header</Text>
<Slot />
<Text>Footer</Text>
</View>
);
}
export default HomeLayout;
Expo Router ondersteunt slechts één layout route per directory. Als je meerdere layout routes wil gebruiken moet je meerdere directories aanmaken.
- app
- _layout.tsx
- home
- _layout.tsx
- index.tsx
Het nadeel hiervan is dat het pad hierdoor "vervuilt" wordt met overbodige directories. Je kan dit probleem oplossen door een deel van de directory structuur te verbergen door een deel van het pad te omringen met ronde haakjes.
app/root/home.tsxkomt overeen met/root/homeapp/(root)/home.tsxkomt overeen met/home
Dit is handig om layouts toe te voegen zonder extra segmenten aan de URL toe te voegen. Je kan zoveel groepen toevoegen als je wil.
Native layouts
Uiteraard wordt er niet vaak gebruik gemaakt van eigen layouts en wordt er vooral gebruik gemaakt van de ingebouwde native layouts van het platform. Expo Router ondersteunt de volgende native layouts:
- Stack Navigation
- Tab Navigation
- Drawer Navigation
- Modals
Stack Navigation
Stack Navigation is de meest gebruikte layout. Het is een layout waarbij de schermen op elkaar gestapeld worden. Je kan navigeren naar een volgend scherm en terugkeren naar het vorige scherm.
Om de schermen te stacken moet je het _layout.tsx bestand aanpassen en een Stack component gebruiken.
import { Stack } from 'expo-router';
const HomeLayout = () => {
return (
<Stack/>
);
}
export default HomeLayout;
Configuratie
Je kan de header van de schermen aanpassen door de screenOptions property te gebruiken van het Stack component.
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
/>

Je kan als children van het Stack component de schermen individueel configureren door ze Stack.Screen componenten te gebruiken. Dit is handig om bijvoorbeeld de titel van de header aan te passen.
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
}}
>
<Stack.Screen name="index" options={{title: "Home"}}></Stack.Screen>
<Stack.Screen name="users/[name]" options={{title: "User"}}></Stack.Screen>
</Stack>
Hierboven gaan we dus de titel van de header aanpassen voor de home pagina en de mische route.
Het is ook mogelijk om het Stack.Screen component te gebruiken in het component zelf. Dus in plaats van de Stack component te gebruiken in het _layout.tsx bestand kan je het Stack.Screen component gebruiken in het index.tsx bestand.
const Index = () => {
return (
<View style={styles.container}>
<Stack.Screen options={{title: "Home"}}></Stack.Screen>
<Text>Home Screen</Text>
</View>
)
}
Dit maakt het mogelijk om de header iets dynamischer te maken met informatie uit de pagina. Dit is bijvoorbeeld interessant voor pagina's met dynamische routes.
const User = () => {
const { name } = useLocalSearchParams<{name: string}>();
return (
<View style={styles.container}>
<Stack.Screen options={{title: "User: " + name}}></Stack.Screen>
<Text>User : { name }</Text>
</View>
)
}
Omdat expo router gebaseerd is op React Navigation kan je alle mogelijke opties voor het Stack.Screen component kan je hier terugvinden.
Animation
Je kan de animatie van de transitie tussen schermen aanpassen door de animation property mee te geven in het options object van het Stack.Screen component.
<Stack.Screen
name="index"
options={{
animation: 'slide_from_right',
}}
/>
De volgende animaties zijn beschikbaar:
default: de standaard animatiefade: fade in en fade outslide_from_right: slide in van rechts en slide out naar rechtsslide_from_left: slide in van links en slide out naar linksslide_from_bottom: slide in van onder en slide out naar ondernone: geen animatie- meer animaties zijn beschikbaar in de documentatie
Tab Navigation
De werking van een Tab Navigation is vergelijkbaar met een Stack Navigation. Het verschil is dat de schermen niet op elkaar gestapeld worden maar dat er een tab bar wordt toegevoegd.
Om een Tab Navigation te gebruiken moet je het _layout.tsx bestand aanpassen en een Tabs component gebruiken.
import { Tabs } from 'expo-router';
const HomeLayout = () => {
return (
<Tabs>
<Tabs.Screen
// Name of the dynamic route.
name="index"
options={{
title: 'Home',
}}
/>
<Tabs.Screen
// Name of the dynamic route.
name="about"
options={{
title: 'About',
}}
/>
</Tabs>
);
}
export default HomeLayout;
Je kan eventueel ook bepaalde paginas onzichtbaar maken uit de tab bar door de href property op null te zetten.
<Tabs.Screen
name="users/[name]"
options={{
href: null
}}
/>
Je kan deze href property ook gebruiken om een andere route te linken aan de tab. Bv als je deze naar een specifieke user wil linken.
Je kan nog meer opties voor het Tabs.Screen component hier terugvinden. Een zeer interessante optie is de tabBarIcon property. Hiermee kan je een icoon toevoegen aan de tab bar.
import { Tabs } from 'expo-router';
import { FontAwesome } from "@expo/vector-icons";
const HomeLayout = () => {
return (
<Tabs>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({color, size}) => <FontAwesome name="home" size={size} color={color} />
}}
/>
<Tabs.Screen
name="about"
options={{
title: 'About',
tabBarIcon: ({color, size}) => <FontAwesome name="adjust" size={size} color={color} />
}}
/>
</Tabs>
);
}
export default HomeLayout;
Een lijst van alle beschikbare icons kan je vinden op https://icons.expo.fyi. Je hoeft hier geen aparte package voor te installeren, deze icons zijn al beschikbaar in `@expo/vector-icons``.
Drawer Navigation
Om een Drawer Navigation te gebruiken moet je het _layout.tsx bestand aanpassen en een Drawer component gebruiken.
import { Drawer } from 'expo-router/drawer';
const Layout = () => {
return (
<Drawer>
<Drawer.Screen
name="index"
options={{
title: 'Home',
}}
/>
<Drawer.Screen
name="about"
options={{
title: 'About',
}}
/>
</Drawer>
);
}
export default Layout;
Je kan nog meer opties voor het Drawer.Screen component hier terugvinden.
Combineren van layouts
Je kan layouts combineren door meerdere layouts te gebruiken in een directory. Bijvoorbeeld een Tab Navigation in combinatie met een Stack Navigation. Stel dat je eerst een pagina hebt zonder tab bar en als je op een bepaalde link klikt krijg je een pagina met een tab bar.
De structuur van de directory ziet er dan als volgt uit:
- app
- _layout.tsx
- home
- _layout.tsx
- screen1.tsx
- screen2.tsx
De _layout.tsx in de app directory ziet er dan als volgt uit:
import { Stack } from 'expo-router';
const Layout = () => {
return (
<Stack>
<Stack.Screen
name="index"
options={{
// Hide the header for all other routes.
headerShown: false,
}}
/>
<Stack.Screen
name="home"
options={{
// Set the presentation mode to modal for our modal route.
presentation: 'modal',
headerShown: false,
}}
/>
</Stack>
);
}
export default Layout;
met de index.tsx:
import { View, Text, Button } from 'react-native';
import { Link } from 'expo-router';
const Home = () => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{fontWeight: "bold", fontSize: 24}}>Welcome to the app</Text>
<Text style={{fontWeight: "100", marginBottom: 20}}>This is a dummy text</Text>
<Link href="/home" replace asChild>
<Button title='Home Screen'/>
</Link>
</View>
);
}
export default Home;
met de home/_layout.tsx:
import { Tabs } from "expo-router"
import { FontAwesome } from "@expo/vector-icons";
const Layout = () => {
return (
<Tabs>
<Tabs.Screen
name="screen1"
options={{
// Hide the header for all other routes.
headerShown: false,
tabBarIcon: ({color, size}) => <FontAwesome name="home" size={size} color={color} />
}}
/>
<Tabs.Screen
name="screen1"
options={{
// Hide the header for all other routes.
headerShown: false,
tabBarIcon: ({color, size}) => <FontAwesome name="address-card" size={size} color={color} />
}}
/>
</Tabs>
)
}
export default Layout;
met de home/screen1.tsx:
import { View, Text } from "react-native";
const Page1 = () => {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ fontWeight: "bold", fontSize: 24 }}>Welcome to screen 1</Text>
<Text style={{ fontWeight: "100", marginBottom: 20 }}>This is screen 1</Text>
</View>
);
}
export default Page1;
useFocusEffect
Bij een Stack of Tab Navigation blijven schermen bestaan als je naar een ander scherm navigeert. Ga je bijvoorbeeld naar een ander tabblad en kom je daarna terug, dan wordt het eerste scherm niet opnieuw aangemaakt. Het was gewoon even niet zichtbaar.
Dat heeft een gevolg voor useEffect. Een useEffect met een lege dependency array wordt maar één keer uitgevoerd: de eerste keer dat het scherm getoond wordt. Keer je later terug naar het scherm, dan wordt die niet opnieuw uitgevoerd.
Meestal is dat precies wat je wil, maar niet altijd. Stel dat je op het ene scherm een lijst toont en op een ander scherm een item aan die lijst toevoegt. Als je terugkeert naar de lijst, wil je die opnieuw ophalen zodat het nieuwe item erbij staat.
Hiervoor heeft Expo Router de useFocusEffect hook. Deze werkt zoals useEffect, maar wordt uitgevoerd telkens het scherm in beeld komt (de focus krijgt). Net zoals bij useEffect kan je een cleanup functie teruggeven. Die wordt uitgevoerd telkens het scherm uit beeld gaat. Dat is handig om bijvoorbeeld een timer te stoppen.
import React, { useCallback, useEffect } from "react";
import { View, Text, StyleSheet } from "react-native";
import { useFocusEffect } from "expo-router";
const Index = () => {
useEffect(() => {
console.log("useEffect: scherm aangemaakt");
}, []);
useFocusEffect(
useCallback(() => {
console.log("useFocusEffect: scherm in beeld");
return () => {
console.log("useFocusEffect: scherm uit beeld");
};
}, [])
);
return (
<View style={styles.container}>
<Text>Home Screen</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
})
export default Index;
Zet dit scherm in een Tab Navigation met nog een tweede tabblad en wissel een paar keer van tabblad. In de terminal waar je npx expo start uitvoerde zie je dat useEffect maar één keer iets logt, terwijl useFocusEffect dat telkens opnieuw doet als je terugkeert.
useCallback
Misschien viel het je op dat we de functie niet rechtstreeks aan useFocusEffect meegeven, maar eerst in useCallback wikkelen. Dat is nodig omdat useFocusEffect de functie opnieuw uitvoert telkens hij een andere functie binnenkrijgt.
Bij elke render van je component wordt een arrow function opnieuw aangemaakt. Voor JavaScript is dat telkens een nieuwe functie, ook al staat er exact dezelfde code in. Zonder useCallback zou het effect dus na elke render opnieuw uitgevoerd worden in plaats van enkel wanneer het scherm in beeld komt. Pas je in het effect de state aan, dan krijg je zelfs een oneindige lus.
useCallback lost dat op: het onthoudt de functie tussen de renders door en geeft telkens dezelfde functie terug. Enkel als een waarde in de dependency array verandert, wordt er een nieuwe functie gemaakt. Die dependency array werkt dus net zoals bij useEffect: gebruik je in de functie een waarde die kan veranderen (bv. een parameter uit useLocalSearchParams), dan zet je die in de array.
Data opnieuw ophalen
Het meest gebruikte voorbeeld van useFocusEffect is data opnieuw ophalen als je terugkeert naar een scherm. Net zoals bij useEffect mag de functie zelf niet async zijn. Daarom maken we binnenin een aparte async functie die we meteen oproepen.
const [todos, setTodos] = useState<Todo[]>([]);
useFocusEffect(
useCallback(() => {
const loadTodos = async () => {
// haal hier de todos opnieuw op (bv. uit AsyncStorage of Supabase)
// en zet ze in de state met setTodos(...)
};
loadTodos();
}, [])
);