Building a Simple Quiz App with React Native: Navigation and Animation
Introduction:
In this tutorial, we will explore how to create a quiz app using React Native. We’ll cover how to handle navigation between screens and incorporate animations to enhance the user experience. By the end of this tutorial, you’ll have a fully functional quiz app that you can customize and build upon.
Here is quick Demo video to it:
Prerequisites:
- Basic knowledge of React Native and JavaScript
- A development environment set up for React Native
The app includes 5 components in total
- Welcome screen component
- QuizPage component
- Questions component
- ProgressBar component
- Result screen component
Step 1:
- Setting up the Project Start by setting up a new React Native project using the Expo CLI or your preferred setup method. Once the project is set up, we can proceed to the next step.
npx create-expo-app quiz-app
cd quiz-app
Step 2:
- Installing Dependencies Install the necessary dependencies for navigation and animations. We’ll be using React Navigation for navigation and React Native Animated API for animations. Run the following commands in your project directory:
npm install @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler
Step3: Navigation
In the App.js file import the NavigationContainer and createNativeStackNavigator and initialize the Stack. Then wrap all the screens with <Stack.Screen>
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import Welcome from "./app/screens/Welcome";
import QuizPage from "./app/screens/QuizPage";
import Result from "./app/screens/Result";
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Welcome">
<Stack.Screen
name="Welcome"
component={Welcome}
options={{ headerShown: false }}
/>
<Stack.Screen
name="Quiz"
component={QuizPage}
options={{
title: "Questions",
headerStyle: {
backgroundColor: "#fac782",
},
headerTintColor: "#fff",
headerTitleStyle: {
fontWeight: "bold",
},
}}
/>
<Stack.Screen
name="Result"
component={Result}
options={{
headerShown: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
Step 4: Creating Screens
- Before starting with screens create QuizData.js in the root dir of your app and add the following to question which we will use in our app.
export default data = [
{
question: "What should you do when approaching a yellow traffic light?",
options: [
"Speed up and cross the intersection quickly",
"Come to a complete stop",
"Slow down and prepare to stop",
"Ignore the light and continue driving",
],
correct_option: "Slow down and prepare to stop",
},
{
question: "What does a red octagonal sign indicate?",
options: [
"Yield right of way",
"Stop and proceed when safe",
"Merge with traffic",
"No left turn allowed",
],
correct_option: "Stop and proceed when safe",
},
{
question: "What is the purpose of a crosswalk?",
options: [
"A designated area for parking",
"A place to stop and rest",
"A path for pedestrians to cross the road",
"A location for U-turns",
],
correct_option: "A path for pedestrians to cross the road",
},
];
- Create Welcome Screen which shows a image of driver in the centre, text and button — to start the quiz. Also create a startQuiz() which initiates a animation while navigating to QuizPage screen to take the test or on click of Start Quiz button.
here is a full code snippet:
import React, { useState } from "react";
import {
View,
Text,
Image,
TouchableOpacity,
StyleSheet,
Animated,
} from "react-native";
const Welcome = ({ navigation }) => {
const [fadeAnim, setFadeAnim] = useState(new Animated.Value(1));
//this is will initiate a fade animation when user clicks on Let's begin button
//and navigate the user to QuizPage
const startQuiz = () => {
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1900,
useNativeDriver: false,
}),
]).start();
};
return (
<View style={styles.container}>
<Image style={styles.image} source={require("../assets/welcome.png")} />
<View style={styles.subContainer}>
<Text style={styles.text}>Ready For your Written Test?</Text>
</View>
<TouchableOpacity
onPress={() => {
navigation.navigate("Quiz");
startQuiz();
}}
style={styles.btn}
>
<Text style={styles.btnText}>Let's Begin</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#38588b",
alignItem: "center",
justifyContent: "center",
},
image: {
width: "100%",
height: 350,
resizeMode: "contain",
},
subContainer: {
flexDirection: "row",
justifyContent: "center",
alignItem: "center",
marginVertical: 20,
marginHorizontal: 20,
},
text: {
fontSize: 25,
fontWeight: "bold",
color: "#ffffff",
},
btn: {
backgroundColor: "#fac782",
paddingHorizontal: 5,
paddingVertical: 15,
position: "relative",
borderRadius: 15,
marginHorizontal: 20,
alignItems: "center",
},
btnText: {
fontSize: 20,
textAlign: "center",
color: "#ffffff",
letterSpacing: 1.1,
},
});
export default Welcome;
that’s how Welcome Screen would look like
Step 5:
- In this Step we will Quiz Screen — which further contains to separate components ProgressBar and Questions
- Let’s start with creating Questions.js file. In this component we will create a View which will show question number (counter) and a question, both are taken as props from QuizPage.js Screen (which we will create later). here is a code snippet
import React from "react";
import { View, StyleSheet, Text } from "react-native";
import data from "../../QuizData";
const Questions = ({ index, question }) => {
return (
<View style={{}}>
{/* Question Counter */}
<View
style={{
flexDirection: "row",
alignItems: "flex-end",
}}
>
<Text
style={{ color: "#333", fontSize: 15, opacity: 0.6, marginRight: 2 }}
>
{index + 1}
</Text>
<Text style={{ color: "#333", fontSize: 13, opacity: 0.6 }}>
/ {data.length}
</Text>
</View>
{/* Question */}
<Text
style={{
color: "#333",
fontSize: 18,
textAlign: "center",
}}
>
{question}
</Text>
</View>
);
};
const styles = StyleSheet.create({});
export default Questions;
- Create a new file name ProgressBar.js. Which is going to show the user progress — how many questions user has answered out of total questions. This component contains View and Animation.View to the progress bar. here is a code snippet-
import React from "react";
import { View, Animated, StyleSheet } from "react-native";
import data from "../../QuizData";
const ProgressBar = ({ progress }) => {
//quiz data file imported to get the total number of questions
const allQuestions = data;
const progressAnim = progress.interpolate({
inputRange: [0, allQuestions.length],
outputRange: ["0%", "100%"],
}); //length of progress is initialized with 0 and will go to total length of ques
return (
<View style={styles.progressBarContainer}>
<Animated.View
style={[
{
height: 5,
borderRadius: 5,
backgroundColor: "#EDA276" + "90",
},
{
width: progressAnim,
},
]}
></Animated.View>
</View>
);
};
const styles = StyleSheet.create({
progressBarContainer: {
width: "80%",
height: 5,
borderRadius: 5,
backgroundColor: "#00000020",
marginBottom: 10,
},
});
export default ProgressBar;
- Create a new file named QuizPage.js. It is the main screen where user will answer all the Questions by selecting the Options, would display if it is right or wrong, and result is getting calculated in the background and would display on Result Screen (which we will create just after this)
- Will start with the basic UI of this screen which call the component we have created in our previous steps- Questions and ProgressBar. Included a ScrollView to make the screen scrollable
const QuizPage = ({ navigation }) => {
const allQuestions = data;
const [progress, setProgress] = useState(new Animated.Value(1));
const [fadeAnim, setFadeAnim] = useState(new Animated.Value(1));
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [isOptionsDisabled, setIsOptionsDisabled] = useState(false);
const [currentOptionSelected, setCurrentOptionSelected] = useState(null);
const [correctOption, setCorrectOption] = useState(null);
const [score, setScore] = useState(0);
return (
<ScrollView style={styles.scrollView}>
<View style={styles.container}>
<View style={styles.subContainer}>
<ProgressBar progress={progress} />
<Questions
index={currentQuestionIndex}
question={allQuestions[currentQuestionIndex]?.question}
/>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
scrollView: { backgroundColor: "#38588b" },
container: {
flex: 1,
paddingVertical: 20,
paddingHorizontal: 20,
position: "relative",
},
subContainer: {
marginTop: 50,
marginVertical: 10,
padding: 40,
borderTopRightRadius: 40,
borderRadius: 10,
backgroundColor: "white",
alignItems: "center",
shadowColor: "#171717",
shadowOffset: { width: -6, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 3,
},
})
- After displaying progress bar and question with a counter on it. Let’s starting creating our next module which is displaying Options with respect to that question. Lets create a sub component in QuizPage.js file called renderOptions which is going to loop through the array of options. Will display these options inside TouchableOpacity which will act as a button and onPress of it will display the result of on-spot.
- And Call this component just after Questions Component called in QuizPage return. here is a code snippet
const renderOptions = (navigation) => {
return (
<View style={{ marginTop: 100 }}>
{allQuestions[currentQuestionIndex]?.options.map((option, index) => (
<Animated.View
key={index}
style={{
opacity: fadeAnim,
transform: [
{
translateY: fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [(150 / 4) * (index + 10), 0], // 0 : 150, 0.5 : 75, 1 : 0
}),
},
],
}}
>
<TouchableOpacity
onPress={() => validateAnswer(option, navigation)}
key={index}
style={[
{ ...styles.optionsText },
{
backgroundColor: isOptionsDisabled
? option == correctOption
? "#7be25b"
: option == currentOptionSelected
? "#f0222b" //red
: "#cfcdcc" //gray
: "#fac782",
},
]}
>
<Text
style={{
fontSize: 16,
color: "black",
textAlign: "center",
}}
>
{option}
</Text>
</TouchableOpacity>
</Animated.View>
))}
</View>
);
};
//CSS for optionsText...
optionsText: {
borderRadius: 5,
alignItems: "center",
justifyContent: "center",
padding: 10,
paddingHorizontal: 30,
marginVertical: 10,
shadowColor: "#171717",
shadowOffset: { width: -3, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 3,
},
- Time to create validateAnswer() and handleNext(). Where validateAnswer function validates the answers and show right or wrong option to user, increment the user score on every correct answer. handleNext function will show the next question once the current question is answered. here is code snippet:
const validateAnswer = (selectedOption, navigation) => {
if (isOptionsDisabled == false) {
let correct_option = allQuestions[currentQuestionIndex]["correct_option"];
setCurrentOptionSelected(selectedOption);
setCorrectOption(correct_option);
setIsOptionsDisabled(true);
if (selectedOption == correct_option) {
setScore(score + 1);
}
}
};
const handleNext = (navigation) => {
if (currentQuestionIndex == allQuestions.length - 1) {
navigation.navigate("Result", { score: score });
} else {
setCurrentQuestionIndex(currentQuestionIndex + 1);
setCurrentOptionSelected(null);
setCorrectOption(null);
setIsOptionsDisabled(false);
}
Animated.parallel([
Animated.timing(progress, {
toValue: currentQuestionIndex + 2,
duration: 2000,
useNativeDriver: false,
}),
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1900,
useNativeDriver: false,
}),
]),
]).start();
};
- It’s time to put all the pieces together and run the code. here is full code snippet of QuizPage and a screenshot how it would look like
import React, { useState } from "react";
import {
View,
Text,
ScrollView,
Animated,
StyleSheet,
TouchableOpacity,
} from "react-native";
import data from "../../QuizData";
import ProgressBar from "./ProgressBar";
import Questions from "./Questions";
const QuizPage = ({ navigation }) => {
const allQuestions = data;
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [progress, setProgress] = useState(new Animated.Value(1));
const [fadeAnim, setFadeAnim] = useState(new Animated.Value(1));
const [isOptionsDisabled, setIsOptionsDisabled] = useState(false);
const [currentOptionSelected, setCurrentOptionSelected] = useState(null);
const [correctOption, setCorrectOption] = useState(null);
const [score, setScore] = useState(0);
const restartQuiz = () => {
setCurrentQuestionIndex(0);
setScore(0);
setCurrentOptionSelected(null);
setCorrectOption(null);
setIsOptionsDisabled(false);
};
const validateAnswer = (selectedOption, navigation) => {
if (isOptionsDisabled == false) {
let correct_option = allQuestions[currentQuestionIndex]["correct_option"];
setCurrentOptionSelected(selectedOption);
setCorrectOption(correct_option);
setIsOptionsDisabled(true);
if (selectedOption == correct_option) {
setScore(score + 1);
}
}
};
const handleNext = (navigation) => {
if (currentQuestionIndex == allQuestions.length - 1) {
navigation.navigate("Result", { score: score });
} else {
setCurrentQuestionIndex(currentQuestionIndex + 1);
setCurrentOptionSelected(null);
setCorrectOption(null);
setIsOptionsDisabled(false);
}
Animated.parallel([
Animated.timing(progress, {
toValue: currentQuestionIndex + 2,
duration: 2000,
useNativeDriver: false,
}),
Animated.sequence([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 100,
useNativeDriver: false,
}),
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1900,
useNativeDriver: false,
}),
]),
]).start();
};
const renderOptions = (navigation) => {
return (
<View style={{ marginTop: 100 }}>
{allQuestions[currentQuestionIndex]?.options.map((option, index) => (
<Animated.View
key={index}
style={{
opacity: fadeAnim,
transform: [
{
translateY: fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [(150 / 4) * (index + 10), 0], // 0 : 150, 0.5 : 75, 1 : 0
}),
},
],
}}
>
<TouchableOpacity
onPress={() => validateAnswer(option, navigation)}
key={index}
style={[
{ ...styles.optionsText },
{
backgroundColor: isOptionsDisabled
? option == correctOption
? "#7be25b"
: option == currentOptionSelected
? "#f0222b" //red
: "#cfcdcc" //gray
: "#fac782",
},
]}
>
<Text
style={{
fontSize: 16,
color: "black",
textAlign: "center",
}}
>
{option}
</Text>
</TouchableOpacity>
</Animated.View>
))}
</View>
);
};
return (
<ScrollView style={styles.scrollView}>
<View style={styles.container}>
<View style={styles.subContainer}>
<ProgressBar progress={progress} />
<Questions
index={currentQuestionIndex}
question={allQuestions[currentQuestionIndex]?.question}
/>
</View>
{renderOptions(navigation)}
</View>
<View style={{ position: "absolute", bottom: -75, right: 20 }}>
<TouchableOpacity
style={[
{ ...styles.btnNext },
{
backgroundColor: !currentOptionSelected ? "#cfcdcc" : "#ffffff",
},
]}
disabled={!currentOptionSelected}
onPress={() => handleNext(navigation)}
>
<Text style={styles.btnNextText}>NEXT</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
scrollView: { backgroundColor: "#38588b" },
container: {
flex: 1,
paddingVertical: 20,
paddingHorizontal: 20,
position: "relative",
},
subContainer: {
marginTop: 50,
marginVertical: 10,
padding: 40,
borderTopRightRadius: 40,
borderRadius: 10,
backgroundColor: "white",
alignItems: "center",
shadowColor: "#171717",
shadowOffset: { width: -6, height: 6 },
shadowOpacity: 0.2,
shadowRadius: 3,
},
optionsText: {
borderRadius: 5,
alignItems: "center",
justifyContent: "center",
padding: 10,
paddingHorizontal: 30,
marginVertical: 10,
shadowColor: "#171717",
shadowOffset: { width: -3, height: 3 },
shadowOpacity: 0.2,
shadowRadius: 3,
},
btnNext: {
borderRadius: 10,
paddingVertical: 13,
paddingHorizontal: 20,
backgroundColor: "#ffffff",
},
btnNextText: {
color: "#333",
fontSize: 17,
letterSpacing: 1.1,
},
});
export default QuizPage;
Step 6:
Now it’s time display the result to user when all questions are answered by user. It contains Your Score as a heading and actual score in numbers (2/3 format), Retry button to let the user take the quiz again. here is code-snippet and a screenshot:
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
const Result = ({ navigation, route }) => {
const { score } = route.params;
return (
<View style={styles.container}>
<View style={styles.subContainer}>
<Text style={{ fontSize: 50 }}>Your Score</Text>
<View style={styles.textWrapper}>
<Text style={styles.score}>{score}</Text>
<Text style={styles.score}> / 3</Text>
</View>
{/* Retry Quiz button */}
<TouchableOpacity
onPress={() => {
navigation.navigate("Welcome");
}}
style={styles.btnReset}
>
<Text style={styles.btnText}>Retry</Text>
</TouchableOpacity>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#38588b",
alignItems: "center",
justifyContent: "center",
},
subContainer: {
backgroundColor: "#38588b",
width: "90%",
borderRadius: 20,
padding: 20,
alignItems: "center",
},
textWrapper: {
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginVertical: 30,
},
score: {
fontSize: 100,
color: "#ffffff",
fontWeight: "bold",
},
btnReset: {
backgroundColor: "#333",
paddingHorizontal: 5,
paddingVertical: 15,
width: "50%",
borderRadius: 15,
},
btnText: {
textAlign: "center",
color: "#ffffff",
fontSize: 20,
letterSpacing: 1,
},
});
export default Result;
In this tutorial, we’ve learned how to build a quiz app using React Native. We covered the basics of navigation using React Navigation and added animations to enhance the user experience. By following the steps outlined in this tutorial, you can create your own quiz app and customize it to fit your needs.
Feel free to explore more features and customize the app further. You can add a timer, store user scores, or even integrate with an external API to fetch quiz questions dynamically. Happy coding!
Source Code can be found here: https://github.com/n-bhasin/ReactNative-QuizApp
If you like it then don’t forget to follow me!