Real apps have many screens. In Expo, the standard way to move between them is Expo Router. It uses file-based routing, like Next.js on the web: each file in the app folder becomes a screen, and its name becomes the route. It is built on React Navigation, so you get native-feeling transitions and a back button for free.
Concepts in this project
- File-based routes:
app/index.tsxis/,app/quiz.tsxis/quiz,app/[id].tsxis/123. - _layout.tsx: wraps the screens in a navigator.
Stackpushes screens on top of each other. - useRouter:
router.pushgoes forward,router.replaceswaps the current screen,router.backgoes back. - Params: send values with
{ pathname, params }and read them withuseLocalSearchParams. Params are always strings. - Keep data out of `app/`: every file there is treated as a route, so put helpers in a
srcfolder.
Set up Expo Router
A new project made with the default Expo template already uses Expo Router. If you started from the blank template, follow these steps.
package.json, app.json and folder layout
// package.json
{
"main": "expo-router/entry"
}
// app.json (add the scheme and the plugin)
{
"expo": {
"scheme": "myapp",
"plugins": ["expo-router"]
}
}
// File layout for expo-router: every file in app/ is a screen
app/
_layout.tsx navigation container (Stack or Tabs)
index.tsx route "/"
quiz.tsx route "/quiz"
[id].tsx dynamic route "/123"
Install the supporting libraries with the commands below (use only the lines you need now), and delete the old App.tsx so it does not conflict.
Install commands for all ten projects
# Install libraries with expo install so versions match your Expo SDK
npx expo install @react-native-async-storage/async-storage # projects 2, 5, 8, 9
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar # projects 3, 7, 10
npx expo install expo-sqlite # project 6
npx expo install expo-image-picker expo-file-system # project 8
npx expo install expo-notifications # project 9
npm install @tanstack/react-query zustand # projects 7, 10
Each project tells you which line to run.
my-app/
app/
_layout.tsx Stack navigator
index.tsx home: Start button
quiz.tsx questions
result.tsx final score
src/
questions.ts the quiz data
The code
`src/questions.ts`
export type Question = { text: string; options: string[]; answer: number };
export const questions: Question[] = [
{ text: "Which company created React Native?", options: ["Google", "Meta", "Apple", "Microsoft"], answer: 1 },
{ text: "Which component shows a scrollable list efficiently?", options: ["View", "Text", "FlatList", "Image"], answer: 2 },
{ text: "Which hook keeps a value between renders?", options: ["useState", "useRouter", "useId", "useContext"], answer: 0 },
{ text: "What language is used with Expo in this course?", options: ["Java", "TypeScript", "Swift", "PHP"], answer: 1 },
];
`app/_layout.tsx`
import { Stack } from "expo-router";
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Quiz" }} />
<Stack.Screen name="quiz" options={{ title: "Question", headerBackVisible: false }} />
<Stack.Screen name="result" options={{ title: "Result", headerBackVisible: false }} />
</Stack>
);
}
`app/index.tsx`
import { View, Text, Pressable, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { questions } from "../src/questions";
export default function Home() {
const router = useRouter();
return (
<View style={styles.screen}>
<Text style={styles.title}>React Native Quiz</Text>
<Text style={styles.sub}>{questions.length} questions</Text>
<Pressable style={styles.button} onPress={() => router.push("/quiz")}>
<Text style={styles.buttonText}>Start</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
title: { fontSize: 30, fontWeight: "700" },
sub: { marginVertical: 12, color: "#555" },
button: { backgroundColor: "#3b5bdb", paddingVertical: 14, paddingHorizontal: 40, borderRadius: 12 },
buttonText: { color: "#fff", fontSize: 18, fontWeight: "600" },
});
`app/quiz.tsx`
import { useState } from "react";
import { View, Text, Pressable, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { questions } from "../src/questions";
export default function Quiz() {
const router = useRouter();
const [index, setIndex] = useState(0);
const [score, setScore] = useState(0);
const [picked, setPicked] = useState<number | null>(null);
const q = questions[index];
const last = index === questions.length - 1;
const choose = (i: number) => {
if (picked !== null) return;
setPicked(i);
if (i === q.answer) setScore((s) => s + 1);
};
const next = () => {
if (last) {
router.replace({ pathname: "/result", params: { score: String(score), total: String(questions.length) } });
} else {
setIndex(index + 1);
setPicked(null);
}
};
return (
<View style={styles.screen}>
<Text style={styles.count}>
Question {index + 1} of {questions.length}
</Text>
<Text style={styles.question}>{q.text}</Text>
{q.options.map((opt, i) => {
const state = picked === null ? "" : i === q.answer ? "right" : i === picked ? "wrong" : "";
return (
<Pressable
key={opt}
onPress={() => choose(i)}
style={[styles.option, state === "right" && styles.right, state === "wrong" && styles.wrong]}
>
<Text style={styles.optionText}>{opt}</Text>
</Pressable>
);
})}
{picked !== null && (
<Pressable style={styles.next} onPress={next}>
<Text style={styles.nextText}>{last ? "See result" : "Next"}</Text>
</Pressable>
)}
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24 },
count: { color: "#555", marginBottom: 8 },
question: { fontSize: 22, fontWeight: "600", marginBottom: 20 },
option: { padding: 14, borderRadius: 10, backgroundColor: "#e9ecef", marginBottom: 10 },
optionText: { fontSize: 16 },
right: { backgroundColor: "#b2f2bb" },
wrong: { backgroundColor: "#ffc9c9" },
next: { marginTop: 16, backgroundColor: "#3b5bdb", padding: 14, borderRadius: 10, alignItems: "center" },
nextText: { color: "#fff", fontWeight: "600", fontSize: 16 },
});
`app/result.tsx`
import { View, Text, Pressable, StyleSheet } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
export default function Result() {
const { score, total } = useLocalSearchParams<{ score: string; total: string }>();
const router = useRouter();
return (
<View style={styles.screen}>
<Text style={styles.title}>
You scored {score} out of {total}
</Text>
<Pressable style={styles.button} onPress={() => router.replace("/")}>
<Text style={styles.buttonText}>Play again</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
title: { fontSize: 26, fontWeight: "700", textAlign: "center", marginBottom: 24 },
button: { backgroundColor: "#3b5bdb", paddingVertical: 14, paddingHorizontal: 36, borderRadius: 12 },
buttonText: { color: "#fff", fontSize: 18, fontWeight: "600" },
});
How it works
- Start calls
router.push("/quiz"). The stack now holds home and quiz. - The quiz keeps
index,scoreand the chosen answer in state. After the last question it callsrouter.replaceto/resultwith the score as params.replaceswaps the quiz out so Back does not return to it. - The result screen reads
scoreandtotalwithuseLocalSearchParamsand offers Play again withrouter.replace("/").
Expected output
The home screen shows React Native Quiz, "4 questions" and a Start button. Each question shows Question 1 of 4, the text and four options. Tapping an option turns the right answer green and a wrong choice red, and a Next button appears. After the last question you see "You scored 3 out of 4" (your number depends on your answers) and a Play again button. The back arrow is hidden on the quiz and result screens by design.
Your task
- Set up the router, add the files and play the quiz.
- Add four more questions to
src/questions.ts. - Show a different message for a perfect score on the result screen.
- Add a Settings screen reachable from home and check that the back button returns to home.
How to check it worked
- The quiz runs from start to result.
- Back does not return into a finished quiz.
- You can explain push versus replace.