Video Tutorial (Optional)
Watch first if you want to see audio recording and playback working end to end in a React Native Expo app.
Project Overview
React Native Expo + expo-av Audio: In this tutorial, you build a React Native Expo app that uses the expo-av Audio module (plus Expo FileSystem) to record audio, save it, and play it back from a single button.
This pattern is useful for language learning apps, voice notes, podcast prototypes, and any app that needs basic voice capture.
- Time: 15 to 30 minutes
- Skill level: Beginner
- What you will build: A simple Expo app that requests mic permission, records audio, saves it locally, and plays it back
Parts List
From ShillehTek
- No ShillehTek-specific parts required for this software build.
External
- Node.js and npm - required to run Expo tooling and install dependencies
- Expo CLI - used to initialize an Expo project
- React Native Expo app project - the app you will create
-
expo-av- provides recording and playback APIs -
expo-file-system- used to create a folder and move the recorded file -
@expo/vector-icons- used for the record/stop icon
Note: The Expo audio library can be buggy in the simulator. You may need to close and reopen the simulator, and make sure the simulator volume is turned up.
Step-by-Step Guide
Step 1 - Initialize an Expo app
Goal: Create a new Expo project you can run on a device or simulator.
What to do: Install Node.js and npm if you do not already have them: https://nodejs.org/en/download/.
Then install Expo CLI globally and initialize a new project.
Code:
npm install -g expo-cli
expo init my-new-app
Replace my-new-app with your preferred app name, then pick a template (blank is fine).
Expected result: A new Expo project folder is created and dependencies are installed.
Step 2 - Add the audio recording and playback code to your component
Goal: Request microphone permission, record audio, save it to the app filesystem, then play it back.
What to do: Add the following code to your component (for example, your App.js). This example uses a single button to toggle record and stop.
Code:
import { Text, TouchableOpacity, View, StyleSheet } from 'react-native';
import React, { useState, useEffect } from 'react';
import { Audio } from 'expo-av';
import * as FileSystem from 'expo-file-system';
import { FontAwesome } from '@expo/vector-icons';
export default function App() {
const [recording, setRecording] = useState(null);
const [recordingStatus, setRecordingStatus] = useState('idle');
const [audioPermission, setAudioPermission] = useState(null);
useEffect(() => {
// Simply get recording permission upon first render
async function getPermission() {
await Audio.requestPermissionsAsync().then((permission) => {
console.log('Permission Granted: ' + permission.granted);
setAudioPermission(permission.granted)
}).catch(error => {
console.log(error);
});
}
// Call function to get permission
getPermission()
// Cleanup upon first render
return () => {
if (recording) {
stopRecording();
}
};
}, []);
async function startRecording() {
try {
// needed for IoS
if (audioPermission) {
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true
})
}
const newRecording = new Audio.Recording();
console.log('Starting Recording')
await newRecording.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY);
await newRecording.startAsync();
setRecording(newRecording);
setRecordingStatus('recording');
} catch (error) {
console.error('Failed to start recording', error);
}
}
async function stopRecording() {
try {
if (recordingStatus === 'recording') {
console.log('Stopping Recording')
await recording.stopAndUnloadAsync();
const recordingUri = recording.getURI();
// Create a file name for the recording
const fileName = `recording-${Date.now()}.caf`;
// Move the recording to the new directory with the new file name
await FileSystem.makeDirectoryAsync(FileSystem.documentDirectory + 'recordings/', { intermediates: true });
await FileSystem.moveAsync({
from: recordingUri,
to: FileSystem.documentDirectory + 'recordings/' + `${fileName}`
});
// This is for simply playing the sound back
const playbackObject = new Audio.Sound();
await playbackObject.loadAsync({ uri: FileSystem.documentDirectory + 'recordings/' + `${fileName}` });
await playbackObject.playAsync();
// resert our states to record again
setRecording(null);
setRecordingStatus('stopped');
}
} catch (error) {
console.error('Failed to stop recording', error);
}
}
async function handleRecordButtonPress() {
if (recording) {
const audioUri = await stopRecording(recording);
if (audioUri) {
console.log('Saved audio file to', savedUri);
}
} else {
await startRecording();
}
}
return (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={handleRecordButtonPress}>
<FontAwesome name={recording ? 'stop-circle' : 'circle'} size={64} color="white" />
</TouchableOpacity>
<Text style={styles.recordingStatusText}>{`Recording status: ${recordingStatus}`}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
alignItems: 'center',
justifyContent: 'center',
width: 128,
height: 128,
borderRadius: 64,
backgroundColor: 'red',
},
recordingStatusText: {
marginTop: 16,
},
});
Expected result: Pressing the button starts recording, pressing again stops the recording, saves it to a recordings folder, and plays it back.
Step 3 - Understand what the main functions do
Goal: Know where to customize behavior (permissions, start, stop, and playback).
What to do: Review the responsibilities of each section:
- useEffect() requests recording permission on first render and cleans up an active recording during unmount.
- startRecording() sets the iOS audio mode, prepares a new recording, and starts it.
-
stopRecording() stops the recording, generates a filename, creates a
recordings/directory, moves the file there, then loads and plays it back. - handleRecordButtonPress() toggles between start and stop based on whether a recording exists.
The rest of the file is UI layout and styling, which you can keep as-is or customize.
Expected result: You can identify where permission, recording, file saving, and playback happen in the code.
Conclusion
You built a React Native Expo app using the expo-av Audio module that records audio, saves it to the device filesystem, and plays it back on demand. This gives you a solid starting point for voice notes, audio messaging, and podcast style features.
Want parts and tools for your next build? Shop at ShillehTek.com. If you want help tailoring a mobile plus IoT experience or building a custom solution for your product, check out our consulting services.


