Build a Quran Reader App with React Native
Build a multi-screen Quran Reader mobile app with Expo and React Native.
Introduction
30 Second Summary
Millions of people around the world read and listen to the Quran every day, whether during prayer, study, or on their commute. Having a dedicated app that combines reading with audio recitation makes this practice accessible anywhere.
In this project, you will build a Quran Reader mobile app using React Native and Expo. The app lets users browse all 114 surahs, read ayahs in Arabic with English translations, and play audio recitation for each ayah.
What You'll Build
A two-screen mobile app with a surah list, a surah reading view with per-ayah audio playback, and stack navigation connecting them.
By the end of this project, you'll have:
- A Surah List screen displaying all 114 surahs with Arabic names, English transliteration, translation, ayah count, and revelation type.
- A Surah Reading screen showing Arabic text and English translation for each ayah, with a play button for audio recitation.
- Stack navigation between screens using Expo Router with dynamic routing to load any surah by number.
- Secret Mission: Add a search bar to filter surahs by name or number on the list screen.
Are there any prerequisites?
You should have basic familiarity with React (components, props, state, and hooks). You also need Node.js and npm installed on your machine.
For testing, you need a physical iPhone or Android phone with Expo Go installed (free download from your app store), or Xcode for iOS Simulator.
Set Up Your Expo Project
Every mobile app starts with a foundation. Before you can build screens, fetch data, or play audio, you need a project scaffold that handles the build tooling and device connectivity for you.
In this step, you will create a new Expo project, install the audio library you will need later, and configure the root layout with a navigation header. By the end, your app will be running live on your phone or simulator.
In this step, get ready to:
- Create a new Expo project from the SDK 56 template and run it on your device.
- Install the expo-audio library for audio playback.
- Configure the root layout with a Stack navigator and a green header.
Create a new Expo project and run it
Expo is a React Native framework that handles the build tooling, native module linking, and file-based routing so you can focus on writing your app. You will use the create-expo-app CLI to scaffold a new project.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Expo project by running this command:
npx create-expo-app@latest QuranReader --template default@sdk-56
What does this command do?
npx create-expo-app@latest downloads and runs the latest version of the Expo project creator.
QuranReader is the name of your project folder. The --template default@sdk-56 flag tells it to use Expo SDK 56, which includes Expo Router and TypeScript pre-configured.
- If your terminal asks you to install the create-expo-app package, type y and press Enter to confirm.
The scaffolding takes a minute or two while it downloads dependencies. Once it finishes, you will see a success message in the terminal.
- Navigate into your new project folder by running:
cd QuranReader
The default template comes with example screens and boilerplate you do not need. The reset-project script clears this out, leaving only the two core files you will build on.
- Clear the boilerplate by running:
npm run reset-project
What did the reset script do?
The script moved the example code into an app-example folder and left only index.tsx and _layout.tsx inside your src/app directory. You can delete app-example later since it is not part of your app.
- Start the development server by running:
npx expo start
You will see a QR code and a list of keyboard shortcuts in the terminal.
- To open the app on a physical device, scan the QR code with the default Camera app (iOS) or use the Scan QR code option inside Expo Go (Android). Make sure your phone and computer are on the same Wi-Fi network.
- To open the app in iOS Simulator instead, press i in the terminal.
You should see a blank screen with a default welcome message. The app is running.
App not loading on your device?
Make sure your phone and computer are connected to the same Wi-Fi network. Public or corporate networks sometimes block device discovery.
If you are using a physical device and the QR code does not connect, try restarting the server with npx expo start --tunnel to route through a tunnel instead of local networking.
help me troubleshoot why my Expo app is not loading on my device after running npx expo start
Install the expo-audio library
Your Quran Reader will play audio recitations for each ayah. The expo-audio library provides a React hook for audio playback that automatically manages the player lifecycle.
- Stop the development server by pressing Ctrl+C in the terminal.
- Install the audio library by running:
npx expo install expo-audio
- Confirm the install completed without errors. You should see a success message in the terminal output.
Install failed?
Make sure you stopped the dev server before running the install command. If you see permission errors, try running the command again with sudo on macOS or opening the terminal as Administrator on Windows.
help me fix an error when running npx expo install expo-audio
Set up the root layout with a Stack navigator
Every Expo Router app has a root layout file that controls which navigator wraps your screens. A Stack navigator displays screens one on top of another with a header bar and a back button, which is exactly the pattern your two-screen Quran Reader needs.
- Open your code editor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type the name of your editor (e.g. Cursor or VS Code) and press Enter to open it.
- Open the QuranReader folder from your Desktop. In your editor, click File in the top menu bar.
- Click Open Folder.
- Select the QuranReader folder on your Desktop, then click Open.
- In the file explorer sidebar, expand src then app, and click on _layout.tsx to open it.
- Select all the existing code in the file and replace it with the following:
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack
screenOptions={{
// Set a dark green background for the navigation header
headerStyle: { backgroundColor: '#1B5E20' },
// Make header text white for contrast
headerTintColor: '#FFFFFF',
// Bold the header title text
headerTitleStyle: { fontWeight: 'bold' },
}}
/>
);
}
What does this code do?
The Stack component from expo-router creates a native stack navigator. Every screen you add as a file inside src/app/ automatically becomes a route in this stack.
The screenOptions prop sets a consistent look across all screens. Here you are giving the header a dark green background (#1B5E20), white text (#FFFFFF), and bold title styling.
- Save _layout.tsx.
- Switch back to your terminal and restart the development server by running:
npx expo start
- Open the app on your device or simulator again (scan the QR code or press i).
You should see a dark green navigation header at the top of the screen. The screen content below may be blank or show default text.
Don't see the green header?
Double check that you saved _layout.tsx after replacing its content. Unsaved changes will not be picked up by the dev server.
Make sure you replaced ALL the existing code in _layout.tsx, not just part of it. Leftover code from the template can cause import errors.
help me debug why my Stack navigator is not showing a green header in my Expo app
✔️ Awesome, I've got everything!
Great. Double check that you have saved _layout.tsx and your app is running with the green header visible.
ⓧ I'd like to double check the full code
Here is the complete _layout.tsx file. Compare it against yours character by character.
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: '#1B5E20' },
headerTintColor: '#FFFFFF',
headerTitleStyle: { fontWeight: 'bold' },
}}
/>
);
}
Your project is scaffolded, your audio library is installed, and your navigation header is styled. Next up, you will build the Surah List screen that displays all 114 surahs from a live API.
Build the Surah List Screen
Your Expo project is set up with a Stack navigator and a green header. Now you need to fill that screen with actual content.
The Quran has 114 surahs, each with metadata like its Arabic name, English translation, and how many ayahs it contains. In this step, you will fetch that data from a free API and display it as a scrollable, tappable list.
In this step, get ready to:
- Create an API helper file that fetches all 114 surahs.
- Build a scrollable surah list with Arabic names, translations, and metadata.
- Make each surah tappable so it navigates to a reading screen.
Create the API fetching function for surah data
Before building any UI, you need a way to get surah data into your app. The Al Furqan API is a free, public Quran API that returns all 114 surahs with metadata like Arabic names, transliterations, and ayah counts.
You will centralize all your API calls in a single file so the rest of your app can import and use them cleanly.
- Create a new folder inside src by right-clicking the src folder in the Explorer sidebar and selecting New Folder.
- Name the folder lib.
- Right-click the new lib folder, select New File, and name it api.ts.
- Define the TypeScript interfaces for surah and ayah data by pasting this code into api.ts:
// Shape of a single surah from the Al Furqan API
export interface Surah {
number: number;
name: string;
transliteration: string;
translation: string;
ayahCount: number;
revelationType: string;
}
// Shape of a single ayah (used in later steps)
export interface Ayah {
number: number;
text: string;
translation: string;
}
What do these interfaces do?
TypeScript interfaces define the shape of data your app expects. Surah tells TypeScript that every surah object must have a number, name, transliteration, and so on.
The Ayah interface is a placeholder for later steps when you build the surah reading screen. Defining it now keeps all your data types in one place.
- Add the fetchSurahs function below the interfaces:
// Fetch all 114 surahs from the Al Furqan API
export async function fetchSurahs(): Promise<Surah[]> {
const response = await fetch('https://alfurqan.online/api/v1/surahs');
const data = await response.json();
return data.surahs;
}
What does fetchSurahs do?
This function calls the Al Furqan API endpoint at https://alfurqan.online/api/v1/surahs and returns the surahs array from the response. The API requires no authentication and returns all 114 surahs in a single call.
- Save src/lib/api.ts.
You won't see a change in the app yet. The next substep uses this function to display surah data on screen.
Seeing a red underline in api.ts?
Make sure the file is saved inside src/lib/ and not directly inside src/. The folder structure should be src/lib/api.ts.
Help me debug my api.ts file
Build the Surah List UI with FlatList
With the API helper ready, you can now build the main screen. React Native provides a FlatList component for rendering long, scrollable lists efficiently. Unlike ScrollView, FlatList only renders items currently visible on screen.
- Open src/app/index.tsx in the Explorer sidebar.
- Replace all of its contents with the following component that imports the API, fetches surahs on mount, and renders the full list:
import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
Pressable,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import { Link } from 'expo-router';
import { fetchSurahs, Surah } from '@/lib/api';
export default function SurahListScreen() {
// Store the array of surahs and a loading flag
const [surahs, setSurahs] = useState<Surah[]>([]);
const [loading, setLoading] = useState(true);
// Fetch surahs once when the component first mounts
useEffect(() => {
fetchSurahs()
.then(setSurahs)
.catch(console.error)
.finally(() => setLoading(false));
}, []);
// Show a spinner while data is loading
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#1B5E20" />
</View>
);
}
return (
<FlatList
data={surahs}
keyExtractor={(item) => String(item.number)}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
// Link wraps each row for tap-to-navigate
<Link href={`/surah/${item.number}`} asChild>
<Pressable style={styles.surahItem}>
{/* Surah number badge */}
<View style={styles.surahNumber}>
<Text style={styles.numberText}>{item.number}</Text>
</View>
{/* English name and translation */}
<View style={styles.surahInfo}>
<Text style={styles.transliteration}>{item.transliteration}</Text>
<Text style={styles.translation}>{item.translation}</Text>
</View>
{/* Arabic name and metadata */}
<View style={styles.surahMeta}>
<Text style={styles.arabicName}>{item.name}</Text>
<Text style={styles.ayahCount}>{item.ayahCount} ayahs</Text>
<Text style={styles.revelationType}>{item.revelationType}</Text>
</View>
</Pressable>
</Link>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
);
}
What does this code do?
- useState creates two state variables: surahs (the data array) and loading (whether the fetch is still in progress).
- useEffect with an empty dependency array [] runs fetchSurahs() exactly once when the screen first loads.
- ActivityIndicator is React Native's built-in loading spinner. It shows while the API call is in progress.
- FlatList loops through the surahs array. Each row has three sections: a number badge on the left, the English name and translation in the middle, and the Arabic name with metadata on the right.
- Link from Expo Router wraps each item with the asChild prop, which forwards navigation behavior to the Pressable child. You will learn more about this in the next substep.
- Add the StyleSheet below the component's closing brace:
const styles = StyleSheet.create({
// Centered spinner container
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
// Light background for the full list
list: { backgroundColor: '#FAFAFA' },
// Each row: number badge, info, and Arabic metadata side by side
surahItem: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: '#FFFFFF' },
// Circular green badge for surah number
surahNumber: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#E8F5E9', justifyContent: 'center', alignItems: 'center', marginRight: 12 },
numberText: { fontSize: 14, fontWeight: 'bold', color: '#1B5E20' },
// Middle section takes remaining space
surahInfo: { flex: 1 },
transliteration: { fontSize: 16, fontWeight: '600', color: '#212121' },
translation: { fontSize: 13, color: '#757575', marginTop: 2 },
// Right-aligned Arabic name and metadata
surahMeta: { alignItems: 'flex-end' },
arabicName: { fontSize: 18, color: '#1B5E20', textAlign: 'right' },
ayahCount: { fontSize: 11, color: '#9E9E9E', marginTop: 2 },
revelationType: { fontSize: 11, color: '#9E9E9E' },
// Thin line between rows, indented past the number badge
separator: { height: 1, backgroundColor: '#EEEEEE', marginLeft: 68 },
});
How do these styles work?
React Native uses flexbox for layout, just like CSS. The surahItem style sets flexDirection: 'row' to lay out the number badge, info section, and Arabic name side by side.
The surahInfo section uses flex: 1 to take up all remaining space between the number badge and the Arabic name. The arabicName style uses textAlign: 'right' so the Arabic text renders naturally from right to left.
- Save src/app/index.tsx.
- Check your device or simulator. You should see a scrollable list of all 114 surahs, each showing a green number badge, the English name (like Al-Fatihah), the translation (The Opening), the Arabic name, ayah count, and whether it is Meccan or Medinan.
Don't see the surah list?
- Make sure both src/lib/api.ts and src/app/index.tsx are saved.
- Check that the import path in index.tsx says @/lib/api (not ./lib/api). The @/ alias maps to the src/ directory in the Expo default template.
- If you see a blank screen, open your terminal where the dev server is running and check for error messages.
Help me debug why my surah list is not displaying
Make each surah tappable to navigate to the reading screen
You may have noticed that each surah row is already wrapped in a Link component from Expo Router. This is what makes each row tappable and navigable. Time to understand how it works and test it.
What is a dynamic route?
Expo Router uses file-based routing. A file named [number].tsx inside a folder creates a dynamic route. The square brackets tell the router that number is a URL parameter that can change.
When you navigate to /surah/1, Expo Router matches the [number].tsx file and passes 1 as the number parameter. You will create this file in the next step.
Why use Link with asChild?
By default, the Link component wraps its children in a basic text element, which can break your row layout. The asChild prop tells Link to forward its navigation behavior to the first child component instead.
In your code, asChild passes the navigation onPress handler down to the Pressable, so tapping anywhere on the row triggers navigation to /surah/{item.number}.
- Tap any surah in your app (for example, tap Al-Fatihah at the top of the list).
- You should see the screen navigate to a blank page with a green back arrow in the header. This is expected because you have not created the [number].tsx route file yet.
- Tap the back arrow to return to the surah list.
Tapping does nothing?
- Make sure the Link import is from expo-router, not from react-native.
- Confirm that asChild is on the Link component and that Pressable is its direct child.
- Check the href uses backticks (template literal) and not regular quotes.
Help me troubleshoot why tapping a surah does nothing
✔️ Awesome, I've got everything!
Great. Double check that both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for both files at the end of this step.
export interface Surah {
number: number;
name: string;
transliteration: string;
translation: string;
ayahCount: number;
revelationType: string;
}
export interface Ayah {
number: number;
text: string;
translation: string;
}
export async function fetchSurahs(): Promise<Surah[]> {
const response = await fetch('https://alfurqan.online/api/v1/surahs');
const data = await response.json();
return data.surahs;
}
import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
Pressable,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import { Link } from 'expo-router';
import { fetchSurahs, Surah } from '@/lib/api';
export default function SurahListScreen() {
const [surahs, setSurahs] = useState<Surah[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchSurahs()
.then(setSurahs)
.catch(console.error)
.finally(() => setLoading(false));
}, []);
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#1B5E20" />
</View>
);
}
return (
<FlatList
data={surahs}
keyExtractor={(item) => String(item.number)}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<Link href={`/surah/${item.number}`} asChild>
<Pressable style={styles.surahItem}>
<View style={styles.surahNumber}>
<Text style={styles.numberText}>{item.number}</Text>
</View>
<View style={styles.surahInfo}>
<Text style={styles.transliteration}>{item.transliteration}</Text>
<Text style={styles.translation}>{item.translation}</Text>
</View>
<View style={styles.surahMeta}>
<Text style={styles.arabicName}>{item.name}</Text>
<Text style={styles.ayahCount}>{item.ayahCount} ayahs</Text>
<Text style={styles.revelationType}>{item.revelationType}</Text>
</View>
</Pressable>
</Link>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
);
}
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
list: { backgroundColor: '#FAFAFA' },
surahItem: { flexDirection: 'row', alignItems: 'center', padding: 16, backgroundColor: '#FFFFFF' },
surahNumber: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#E8F5E9', justifyContent: 'center', alignItems: 'center', marginRight: 12 },
numberText: { fontSize: 14, fontWeight: 'bold', color: '#1B5E20' },
surahInfo: { flex: 1 },
transliteration: { fontSize: 16, fontWeight: '600', color: '#212121' },
translation: { fontSize: 13, color: '#757575', marginTop: 2 },
surahMeta: { alignItems: 'flex-end' },
arabicName: { fontSize: 18, color: '#1B5E20', textAlign: 'right' },
ayahCount: { fontSize: 11, color: '#9E9E9E', marginTop: 2 },
revelationType: { fontSize: 11, color: '#9E9E9E' },
separator: { height: 1, backgroundColor: '#EEEEEE', marginLeft: 68 },
});
Your surah list is live and tappable. Next up, you will build the reading screen that appears when you tap into a surah, complete with Arabic ayah text and English translations.
Build the Surah Reading Screen
Your surah list is live and tappable. Right now, tapping a surah navigates to a blank screen because the reading route does not exist yet.
In this step, you will build the reading screen that displays each ayah in Arabic with its English translation. This involves fetching data from a second API and rendering it inside a dynamic route that reads the surah number straight from the URL.
In this step, get ready to:
- Create API functions that fetch ayah text and build audio URLs.
- Build a dynamic reading screen that displays Arabic text and English translations for every ayah in a surah.
Create the dynamic route file and fetch ayah data
The surah list screen already fetches surah metadata from the Al Furqan API. For the reading screen, you need a second API that serves the actual ayah text.
You will use the Al Quran Cloud API to fetch both Arabic text and English translations in a single request. You will also add a helper function that builds audio URLs for each ayah.
- In src/lib/api.ts, find the closing brace of the fetchSurahs() function.
- Add these two new functions below it:
export async function fetchAyahs(surahNumber: number): Promise<Ayah[]> {
// Fetch both Arabic text and English translation in one request
const response = await fetch(
`https://api.alquran.cloud/v1/surah/${surahNumber}/editions/quran-uthmani,en.sahih`
);
const data = await response.json();
const arabicEdition = data.data[0];
const englishEdition = data.data[1];
// Combine the Arabic and English data for each ayah
return arabicEdition.ayahs.map((ayah: any, index: number) => ({
number: ayah.numberInSurah,
text: ayah.text,
translation: englishEdition.ayahs[index].text,
}));
}
export function getAyahAudioUrl(
surahNumber: number,
ayahNumber: number
): string {
// Build the Al Furqan audio streaming URL for a specific ayah
return `https://alfurqan.online/api/v1/audio/mishari-al-afasi/surah/${surahNumber}/ayah/${ayahNumber}`;
}
What does this code do?
fetchAyahs() calls the Al Quran Cloud API with two editions in a single request: quran-uthmani for Arabic text and en.sahih for English translation. It then maps each ayah into the Ayah interface shape you already defined.
getAyahAudioUrl() builds the URL for streaming a single ayah's recitation from the Al Furqan API. You will use this function in the next step when you add audio playback.
Why two APIs?
The Al Furqan API provides excellent audio streaming but does not serve ayah text content. The Al Quran Cloud API provides the Arabic text and English translations. Composing data from multiple APIs is a common pattern in real-world apps.
- Save api.ts.
- Check your terminal. The app should reload without any errors.
Seeing a red error in the terminal?
Make sure you pasted the new functions after the closing brace of fetchSurahs(), not inside it. Each function should be its own standalone export at the top level of the file.
Check that the template literal backticks (`, not single quotes) are used in the URL strings.
Help me debug an error in my api.ts file after adding fetchAyahs and getAyahAudioUrl
✔️ Awesome, I've got everything!
Great. Make sure you have saved api.ts before moving on.
ⓧ I'd like to double check the full code
export interface Surah {
number: number;
name: string;
transliteration: string;
translation: string;
ayahCount: number;
revelationType: string;
}
export interface Ayah {
number: number;
text: string;
translation: string;
}
export async function fetchSurahs(): Promise<Surah[]> {
const response = await fetch('https://alfurqan.online/api/v1/surahs');
const data = await response.json();
return data.surahs;
}
export async function fetchAyahs(surahNumber: number): Promise<Ayah[]> {
const response = await fetch(
`https://api.alquran.cloud/v1/surah/${surahNumber}/editions/quran-uthmani,en.sahih`
);
const data = await response.json();
const arabicEdition = data.data[0];
const englishEdition = data.data[1];
return arabicEdition.ayahs.map((ayah: any, index: number) => ({
number: ayah.numberInSurah,
text: ayah.text,
translation: englishEdition.ayahs[index].text,
}));
}
export function getAyahAudioUrl(
surahNumber: number,
ayahNumber: number
): string {
return `https://alfurqan.online/api/v1/audio/mishari-al-afasi/surah/${surahNumber}/ayah/${ayahNumber}`;
}
Now you need a screen to use these functions. In Expo Router, a file named with square brackets (like [number].tsx) creates a dynamic route. The value inside the brackets becomes a URL parameter you can read in the component.
- In the Explorer sidebar, right-click the src/app folder and select New Folder.
- Name the folder surah.
- Right-click the new surah folder and select New File.
- Name the file [number].tsx. Include the square brackets in the filename.
Why the square brackets?
Expo Router uses the filename to define routes. A file at src/app/surah/[number].tsx matches any URL like /surah/1, /surah/42, or /surah/114. The number part becomes a parameter you can read inside your component with useLocalSearchParams.
Build the ayah list UI
With the API functions ready and the route file created, you can now build the reading screen. This component reads the surah number from the URL, fetches the ayah data, and renders each ayah with Arabic text and English translation.
- Add the following code to src/app/surah/[number].tsx. This is the component function with imports, data fetching, and the loading state:
import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { fetchAyahs, getAyahAudioUrl, Ayah } from '@/lib/api';
export default function SurahReadingScreen() {
// Read the surah number from the URL parameter
const { number } = useLocalSearchParams<{ number: string }>();
const surahNumber = parseInt(number, 10);
const [ayahs, setAyahs] = useState<Ayah[]>([]);
const [loading, setLoading] = useState(true);
// Fetch ayah data when the screen mounts
useEffect(() => {
fetchAyahs(surahNumber)
.then((data) => { setAyahs(data); })
.catch(console.error)
.finally(() => setLoading(false));
}, [surahNumber]);
// Show a loading spinner while the API request is in progress
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#1B5E20" />
</View>
);
}
return (
<>
{/* Set the header title dynamically based on the surah number */}
<Stack.Screen options={{ title: `Surah ${number}` }} />
<FlatList
data={ayahs}
keyExtractor={(item) => String(item.number)}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.ayahItem}>
<View style={styles.ayahHeader}>
{/* Ayah number badge */}
<View style={styles.ayahNumberBadge}>
<Text style={styles.ayahNumberText}>{item.number}</Text>
</View>
</View>
{/* Arabic text displayed large and right-to-left */}
<Text style={styles.arabicText}>{item.text}</Text>
{/* English translation below the Arabic */}
<Text style={styles.translationText}>{item.translation}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</>
);
}
What does this code do?
- useLocalSearchParams() reads the number parameter from the URL. When a learner taps Surah 1 on the list screen, the URL becomes /surah/1, and number equals "1".
- The useEffect hook calls fetchAyahs() when the screen first mounts, then stores the results in state.
- Stack.Screen options dynamically sets the navigation header title to show which surah the learner is reading.
- The FlatList renders each ayah with an Arabic text block (right-aligned) and the English translation underneath.
- Below the component function, add the styles:
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
backgroundColor: '#FAFAFA',
},
ayahItem: {
padding: 16,
backgroundColor: '#FFFFFF',
},
ayahHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
ayahNumberBadge: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
ayahNumberText: {
fontSize: 12,
fontWeight: 'bold',
color: '#1B5E20',
},
// Large right-to-left Arabic text
arabicText: {
fontSize: 24,
lineHeight: 42,
textAlign: 'right',
writingDirection: 'rtl',
color: '#212121',
marginBottom: 12,
},
translationText: {
fontSize: 14,
lineHeight: 22,
color: '#616161',
},
separator: {
height: 1,
backgroundColor: '#EEEEEE',
},
});
How does the Arabic text render correctly?
Two style properties handle right-to-left Arabic rendering. textAlign: 'right' aligns the text block to the right side of the screen. writingDirection: 'rtl' tells React Native to lay out the text from right to left, which is how Arabic is naturally read.
- Save [number].tsx.
- In the app on your device, tap Al-Fatihah (Surah 1). You should see 7 ayahs with large Arabic text on the right and English translations below each one.
- The navigation header should display Surah 1 as the title, and a back arrow should appear to return to the list.
Tapping a surah shows a blank screen or an error?
- Double-check that your file is named exactly [number].tsx with the square brackets included. Without them, Expo Router will not match the route.
- Make sure the file is inside src/app/surah/, not directly inside src/app/. The folder structure must match the URL path /surah/[number].
- If you see a network error, check that your phone or simulator has internet access. The fetchAyahs() function needs to reach the Al Quran Cloud API.
- Try restarting the development server by pressing r in the terminal. New route files sometimes require a reload.
Help me debug my surah reading screen in Expo Router
✔️ Awesome, I've got everything!
Great. Make sure you have saved [number].tsx before moving on.
ⓧ I'd like to double check the full code
import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { fetchAyahs, getAyahAudioUrl, Ayah } from '@/lib/api';
export default function SurahReadingScreen() {
const { number } = useLocalSearchParams<{ number: string }>();
const surahNumber = parseInt(number, 10);
const [ayahs, setAyahs] = useState<Ayah[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchAyahs(surahNumber)
.then((data) => { setAyahs(data); })
.catch(console.error)
.finally(() => setLoading(false));
}, [surahNumber]);
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#1B5E20" />
</View>
);
}
return (
<>
<Stack.Screen options={{ title: `Surah ${number}` }} />
<FlatList
data={ayahs}
keyExtractor={(item) => String(item.number)}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View style={styles.ayahItem}>
<View style={styles.ayahHeader}>
<View style={styles.ayahNumberBadge}>
<Text style={styles.ayahNumberText}>{item.number}</Text>
</View>
</View>
<Text style={styles.arabicText}>{item.text}</Text>
<Text style={styles.translationText}>{item.translation}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</>
);
}
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
list: {
backgroundColor: '#FAFAFA',
},
ayahItem: {
padding: 16,
backgroundColor: '#FFFFFF',
},
ayahHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
ayahNumberBadge: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
},
ayahNumberText: {
fontSize: 12,
fontWeight: 'bold',
color: '#1B5E20',
},
arabicText: {
fontSize: 24,
lineHeight: 42,
textAlign: 'right',
writingDirection: 'rtl',
color: '#212121',
marginBottom: 12,
},
translationText: {
fontSize: 14,
lineHeight: 22,
color: '#616161',
},
separator: {
height: 1,
backgroundColor: '#EEEEEE',
},
});
Your reading screen is rendering ayahs with Arabic text and English translations. Next up, you will add audio playback so users can listen to the recitation of each individual ayah.
Add Audio Playback for Ayah Recitation
Your surah reading screen displays Arabic text and English translations beautifully. But reading the Quran is only half the experience.
In this step, you'll add per-ayah audio playback so the learner can listen to professional recitation by Mishari Al-Afasi. You'll build a reusable audio player component using expo-audio, wire it into each ayah row, and add visual feedback so the currently playing ayah stands out.
In this step, get ready to:
- Create a reusable audio player component with play/pause controls.
- Stream Quran recitation audio from the Al Furqan API for every ayah.
- Add visual highlighting so the currently playing ayah is easy to spot.
Create an audio player component
Each ayah needs its own play/pause button that streams audio from a remote URL. Instead of jamming all that logic into the surah reading screen, you'll build a dedicated AyahAudioPlayer component.
- Create a new file at src/components/AyahAudioPlayer.tsx and paste in the following code:
import { useState, useEffect } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import { useAudioPlayer } from 'expo-audio';
// Define the props this component expects from its parent
interface AyahAudioPlayerProps {
audioUrl: string;
isActive: boolean;
onPlay: () => void;
}
export default function AyahAudioPlayer({
audioUrl,
isActive,
onPlay,
}: AyahAudioPlayerProps) {
// Create an audio player tied to the remote audio URL
const player = useAudioPlayer(audioUrl);
const [isPlaying, setIsPlaying] = useState(false);
// Pause this player when a different ayah starts playing
useEffect(() => {
if (!isActive && isPlaying) {
player.pause();
setIsPlaying(false);
}
}, [isActive]);
// Toggle between play and pause when the button is pressed
const handlePress = () => {
if (isPlaying) {
player.pause();
setIsPlaying(false);
} else {
player.seekTo(0);
player.play();
setIsPlaying(true);
onPlay();
}
};
return (
<Pressable onPress={handlePress} style={styles.button}>
<Text style={styles.buttonText}>{isPlaying && isActive ? '⏸' : '▶'}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#E8F5E9',
justifyContent: 'center',
alignItems: 'center',
},
buttonText: {
fontSize: 16,
color: '#1B5E20',
},
});
What does this code do?
The useAudioPlayer hook from expo-audio creates a player tied to the remote audio URL. It automatically releases resources when the component unmounts, so you don't need manual cleanup.
The component accepts three props. audioUrl is the streaming URL for one ayah. isActive tells the component whether it's the currently selected player. onPlay notifies the parent when this player starts, so the parent can deactivate all other players.
The seekTo(0) call before play() is necessary because expo-audio does not auto-reset the playback position when audio finishes. Without it, tapping play a second time would do nothing.
- Save AyahAudioPlayer.tsx.
You won't see a change on screen yet. The next substep wires this component into the surah reading screen.
Wire up the Al Furqan audio API
Your getAyahAudioUrl helper already builds the correct audio URL for any ayah. Now you need to import the AyahAudioPlayer component into the surah reading screen, pass it each ayah's audio URL, and track which ayah is currently playing.
- In src/app/surah/[number].tsx, find the line that imports from @/lib/api:
import { fetchAyahs, getAyahAudioUrl, Ayah } from '@/lib/api';
- Add this import directly below it:
import AyahAudioPlayer from '@/components/AyahAudioPlayer';
- Next, find the two state declarations inside the SurahReadingScreen function:
const [ayahs, setAyahs] = useState<Ayah[]>([]);
const [loading, setLoading] = useState(true);
- Add these two new state variables directly below them:
// Track the surah name for the header
const [surahName, setSurahName] = useState('');
// Track which ayah is currently playing audio
const [playingAyah, setPlayingAyah] = useState<number | null>(null);
- Now find the closing </View> tag for the ayahNumberBadge inside the renderItem function:
<View style={styles.ayahHeader}>
<View style={styles.ayahNumberBadge}>
<Text style={styles.ayahNumberText}>{item.number}</Text>
</View>
</View>
- Add the AyahAudioPlayer component between the closing </View> of the badge and the closing </View> of the header, so the block looks like this:
<View style={styles.ayahHeader}>
<View style={styles.ayahNumberBadge}>
<Text style={styles.ayahNumberText}>{item.number}</Text>
</View>
<AyahAudioPlayer
audioUrl={getAyahAudioUrl(surahNumber, item.number)}
isActive={playingAyah === item.number}
onPlay={() => setPlayingAyah(item.number)}
/>
</View>
What's happening here?
Each ayah gets its own AyahAudioPlayer with three props. audioUrl calls getAyahAudioUrl to build the Al Furqan streaming URL for that specific ayah.
The isActive prop compares playingAyah (the parent's state) to this ayah's number. When a different ayah starts playing, isActive flips to false, and the component's useEffect automatically pauses the old player.
- Save [number].tsx.
- On your device, tap any surah (try Al-Fatihah). You should see a green circular play button on the right side of each ayah's header row.
- Tap the play button on any ayah. You should hear Mishari Al-Afasi's recitation streaming from the Al Furqan API.
No audio playing?
- Make sure your phone's volume is turned up and not on silent mode.
- Check that expo-audio was installed correctly in Step 1. If you see an import error, stop the dev server and run npx expo install expo-audio again.
- Verify the import path matches exactly: @/components/AyahAudioPlayer. A typo in the path causes a silent failure.
Help me debug why audio isn't playing in my AyahAudioPlayer component.
Add visual feedback for the playing state
Audio works, but there's no visual indicator of which ayah is currently playing. A subtle background color change on the active row makes it immediately clear.
- In [number].tsx, find this line inside the renderItem function:
<View style={styles.ayahItem}>
- Replace it with a conditional style array that applies a highlight when the ayah is playing:
<View
style={[
styles.ayahItem,
playingAyah === item.number && styles.ayahPlaying,
]}
>
- Now scroll down to the StyleSheet.create block. Find the ayahItem style:
ayahItem: { padding: 16, backgroundColor: '#FFFFFF' },
- Add the ayahPlaying style directly below it:
// Highlight the currently playing ayah with a green tint
ayahPlaying: { backgroundColor: '#E8F5E9' },
How does the one-at-a-time logic work?
The parent screen holds a single playingAyah state. When any player's onPlay fires, it updates this state to that ayah's number. Every other player's isActive prop becomes false, triggering the useEffect in AyahAudioPlayer that pauses the old player.
The same playingAyah state drives the conditional style. When the number matches, the ayahPlaying style overrides the background color from white to a subtle green.
- Save [number].tsx.
- On your device, tap into a surah and tap play on any ayah. The row should turn light green while audio is playing.
- Tap play on a different ayah. The first ayah should stop playing and lose its green highlight, and the new ayah should start playing with its own highlight.
Highlight not showing?
- Make sure you changed style={styles.ayahItem} to a style array style={[styles.ayahItem, ...]}. Without the square brackets, React Native ignores the second style.
- Check that the style name ayahPlaying in your StyleSheet.create matches exactly what you reference in the JSX. Style names are case-sensitive.
Help me debug why my ayah highlight style isn't working.
✔️ Awesome, I've got everything!
Great. Double check you've saved both AyahAudioPlayer.tsx and [number].tsx.
ⓧ I'd like to double check the full code
Here is the complete code for both files at this point.
import { useState, useEffect } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';
import { useAudioPlayer } from 'expo-audio';
interface AyahAudioPlayerProps {
audioUrl: string;
isActive: boolean;
onPlay: () => void;
}
export default function AyahAudioPlayer({
audioUrl,
isActive,
onPlay,
}: AyahAudioPlayerProps) {
const player = useAudioPlayer(audioUrl);
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
if (!isActive && isPlaying) {
player.pause();
setIsPlaying(false);
}
}, [isActive]);
const handlePress = () => {
if (isPlaying) {
player.pause();
setIsPlaying(false);
} else {
player.seekTo(0);
player.play();
setIsPlaying(true);
onPlay();
}
};
return (
<Pressable onPress={handlePress} style={styles.button}>
<Text style={styles.buttonText}>{isPlaying && isActive ? '⏸' : '▶'}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { width: 36, height: 36, borderRadius: 18, backgroundColor: '#E8F5E9', justifyContent: 'center', alignItems: 'center' },
buttonText: { fontSize: 16, color: '#1B5E20' },
});
import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import { useLocalSearchParams, Stack } from 'expo-router';
import { fetchAyahs, getAyahAudioUrl, Ayah } from '@/lib/api';
import AyahAudioPlayer from '@/components/AyahAudioPlayer';
export default function SurahReadingScreen() {
const { number } = useLocalSearchParams<{ number: string }>();
const surahNumber = parseInt(number, 10);
const [ayahs, setAyahs] = useState<Ayah[]>([]);
const [loading, setLoading] = useState(true);
const [surahName, setSurahName] = useState('');
const [playingAyah, setPlayingAyah] = useState<number | null>(null);
useEffect(() => {
fetchAyahs(surahNumber)
.then((data) => { setAyahs(data); })
.catch(console.error)
.finally(() => setLoading(false));
}, [surahNumber]);
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#1B5E20" />
</View>
);
}
return (
<>
<Stack.Screen options={{ title: `Surah ${number}` }} />
<FlatList
data={ayahs}
keyExtractor={(item) => String(item.number)}
contentContainerStyle={styles.list}
renderItem={({ item }) => (
<View
style={[
styles.ayahItem,
playingAyah === item.number && styles.ayahPlaying,
]}
>
<View style={styles.ayahHeader}>
<View style={styles.ayahNumberBadge}>
<Text style={styles.ayahNumberText}>{item.number}</Text>
</View>
<AyahAudioPlayer
audioUrl={getAyahAudioUrl(surahNumber, item.number)}
isActive={playingAyah === item.number}
onPlay={() => setPlayingAyah(item.number)}
/>
</View>
<Text style={styles.arabicText}>{item.text}</Text>
<Text style={styles.translationText}>{item.translation}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</>
);
}
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
list: { backgroundColor: '#FAFAFA' },
ayahItem: { padding: 16, backgroundColor: '#FFFFFF' },
ayahPlaying: { backgroundColor: '#E8F5E9' },
ayahHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
ayahNumberBadge: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#F5F5F5', justifyContent: 'center', alignItems: 'center' },
ayahNumberText: { fontSize: 12, fontWeight: 'bold', color: '#1B5E20' },
arabicText: { fontSize: 24, lineHeight: 42, textAlign: 'right', writingDirection: 'rtl', color: '#212121', marginBottom: 12 },
translationText: { fontSize: 14, lineHeight: 22, color: '#616161' },
separator: { height: 1, backgroundColor: '#EEEEEE' },
});
Your Quran Reader now lets you browse surahs, read ayahs in Arabic with English translations, and listen to professional recitation with a single tap. That's a complete, functional app built from a single React Native codebase.
Secret mission
Add Surah Search
Your Quran Reader app displays all 114 surahs, but scrolling through the full list every time is slow. Can you add a search bar that filters surahs in real-time as the user types?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses only free APIs, so there are no ongoing costs.
Resources you used:
- The QuranReader project folder on your Desktop.
- The Expo development server (running in your terminal).
- The Expo Go app on your phone (free, installed as a prerequisite).
Keep everything running
No action needed. Choose this if you're still actively building or want to keep testing your Quran Reader app.
- Your Expo development server stays running in the terminal. You can keep scanning the QR code with Expo Go to preview changes.
- Your code is saved in the QuranReader folder on your Desktop.
- The Al Furqan and Al Quran Cloud APIs are free with no authentication, so there is nothing to manage on that side.
Pause - I'll come back to this later
Shut down the development server to free up your terminal. Your code stays saved locally and you can restart anytime.
- Switch to the terminal where the Expo development server is running.
- Press Ctrl+C to stop the server.
- When you're ready to come back, open your terminal, navigate to the QuranReader folder, and run:
npx expo start
Your app picks up right where you left off.
Delete - I don't want to use this again
Remove all project files from your computer. There are no cloud resources to tear down.
- If your Expo development server is still running, press Ctrl+C in the terminal to stop it.
- Delete the project folder by running the command for your operating system.
macOS
rm -rf ~/Desktop/QuranReader
Windows
rmdir /s /q %USERPROFILE%\Desktop\QuranReader
- If you no longer need Expo Go on your phone, you can uninstall it like any other app.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional Quran Reader mobile app with React Native and Expo that runs on both iOS and Android from a single TypeScript codebase.
You've learned how to:
- Built a multi-screen mobile app using Expo Router's file-based navigation with dynamic routes ([number].tsx) to move between a surah list and a reading screen.
- Fetched live data from REST APIs to display all 114 surahs with Arabic text, English translations, and metadata like ayah count and revelation type.
- Integrated native audio playback using expo-audio's useAudioPlayer hook to stream Quran recitation from the Al Furqan API, with visual highlighting for the currently playing ayah.
- Secret Mission: Added real-time surah search and filtering with a custom search bar using TextInput and controlled state.
Ready to quiz yourself?