DEV Community

albert nahas
albert nahas

Posted on • Originally published at leandine.hashnode.dev

Building a Production Audio Recorder with Expo and React Native

Mobile audio recording is a core feature in many modern apps, powering everything from voice memos to podcast tools and user-generated content platforms. If you’re developing a mobile app with React Native, the Expo framework provides a robust set of APIs—especially the expo-av module—that simplifies cross-platform audio capture. But moving from a basic demo to a reliable, production-ready audio recorder means addressing a host of real-world challenges: managing permissions, supporting background recording, handling file storage, and reliably uploading recordings.

Let’s explore how to build a production-grade audio recorder using Expo and React Native, focusing on background recording, file management, and upload strategies. Along the way, you’ll find practical code examples and architectural tips for scalable, maintainable implementations.

Setting Up Expo AV for Audio Recording

The foundation for any React Native audio app is the expo-av package. This library abstracts away platform-specific details and offers a unified API for audio playback and recording.

First, ensure you have the required dependencies:

expo install expo-av
Enter fullscreen mode Exit fullscreen mode

You’ll also need to handle permissions for microphone access. For iOS, update your app.json with the appropriate usage description:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSMicrophoneUsageDescription": "We need access to your microphone to record audio."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

For Android, Expo manages permissions automatically, but you can customize messages in your AndroidManifest.xml if you eject.

Requesting Permissions

Before starting recording, always request user permission:

import { Audio } from 'expo-av';

async function requestAudioPermission() {
  const { status } = await Audio.requestPermissionsAsync();
  if (status !== 'granted') {
    throw new Error('Microphone permission not granted');
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementing the Recorder

The Audio.Recording API is your entry point for capturing audio. Here’s a basic hook to encapsulate recording logic:

import { useRef, useState } from 'react';
import { Audio, AVPlaybackStatus } from 'expo-av';

export function useAudioRecorder() {
  const [isRecording, setIsRecording] = useState(false);
  const [recordingUri, setRecordingUri] = useState<string | null>(null);
  const recordingRef = useRef<Audio.Recording | null>(null);

  async function startRecording() {
    await requestAudioPermission();

    await Audio.setAudioModeAsync({
      allowsRecordingIOS: true,
      playsInSilentModeIOS: true,
      staysActiveInBackground: true, // critical for background recording
    });

    const recording = new Audio.Recording();
    await recording.prepareToRecordAsync(
      Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY
    );
    await recording.startAsync();
    recordingRef.current = recording;
    setIsRecording(true);
  }

  async function stopRecording() {
    const recording = recordingRef.current;
    if (!recording) return;

    await recording.stopAndUnloadAsync();
    const uri = recording.getURI();
    setRecordingUri(uri);
    setIsRecording(false);
    recordingRef.current = null;
    return uri;
  }

  return {
    isRecording,
    recordingUri,
    startRecording,
    stopRecording,
  };
}
Enter fullscreen mode Exit fullscreen mode

UI Integration

A simple recording UI could look like this:

import React from 'react';
import { Button, View, Text } from 'react-native';
import { useAudioRecorder } from './useAudioRecorder';

export default function AudioRecorderScreen() {
  const { isRecording, recordingUri, startRecording, stopRecording } = useAudioRecorder();

  return (
    <View>
      <Button
        title={isRecording ? 'Stop Recording' : 'Start Recording'}
        onPress={isRecording ? stopRecording : startRecording}
      />
      {recordingUri && (
        <Text>Recorded file: {recordingUri}</Text>
      )}
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Supporting Background Recording

One limitation of Expo-managed workflows is that true background recording (while the app is fully backgrounded) may be limited, particularly on iOS. However, by setting staysActiveInBackground: true in Audio.setAudioModeAsync, you maximize the chances that the recording continues if the user briefly switches apps.

For apps requiring robust background recording (e.g., call recorders, long-form interviews), consider:

  • Ejecting to Expo Bare workflow: This lets you use native modules or third-party libraries with deeper background support, like react-native-audio-recorder-player.
  • Minimize interruptions: Educate users about keeping your app in the foreground for uninterrupted recording.
  • Handle interruptions: Listen for app state changes and gracefully stop or pause recording if the app is backgrounded beyond what the OS allows.
import { AppState } from 'react-native';

useEffect(() => {
  const subscription = AppState.addEventListener('change', (nextAppState) => {
    if (nextAppState.match(/inactive|background/)) {
      // Optionally stop or pause recording here
    }
  });
  return () => subscription.remove();
}, []);
Enter fullscreen mode Exit fullscreen mode

Managing Files and Storage

After recording, the audio file lives on the device, typically in a temporary location. For production apps, consider:

  • Moving files to persistent storage: Use FileSystem.moveAsync from expo-file-system.
  • Naming conventions: Generate unique, timestamped filenames to avoid collisions.
  • Cleanup: Provide mechanisms to delete or archive old files to save device space.

Example: Moving a file to the app’s document directory with a unique name.

import * as FileSystem from 'expo-file-system';

async function moveRecordingToDocuments(uri: string): Promise<string> {
  const fileName = `recording_${Date.now()}.m4a`;
  const newPath = FileSystem.documentDirectory + fileName;
  await FileSystem.moveAsync({ from: uri, to: newPath });
  return newPath;
}
Enter fullscreen mode Exit fullscreen mode

Uploading Audio Files

Uploading audio to a server is a common next step. There are a few patterns to ensure reliability:

Simple Direct Upload

For short recordings and reliable networks, upload as soon as the recording is finished:

async function uploadAudioFile(uri: string) {
  const fileInfo = await FileSystem.getInfoAsync(uri);
  const formData = new FormData();
  formData.append('file', {
    uri,
    name: 'audio.m4a',
    type: 'audio/m4a',
  } as any);

  await fetch('https://your-api/upload', {
    method: 'POST',
    body: formData,
    headers: {
      'Content-Type': 'multipart/form-data',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

Handling Offline and Background Uploads

For robust apps, implement upload queues and retry logic:

  • Queue uploads: Store a list of pending uploads in local storage.
  • Retry failed uploads: Use libraries like react-native-background-upload in the bare workflow, or roll your own with background tasks.
  • User feedback: Show upload progress and error messages.

Security and Privacy

  • Encrypt files before upload if sensitive.
  • Request consent before recording or uploading audio.

Audio Playback for Review

Letting users replay their recordings before uploading is a valuable UX feature. expo-av makes this straightforward:

async function playRecording(uri: string) {
  const { sound } = await Audio.Sound.createAsync({ uri });
  await sound.playAsync();
}
Enter fullscreen mode Exit fullscreen mode

Integrate playback controls in your UI for a polished experience.

Testing and Edge Cases

A production-grade audio recorder must handle:

  • Interrupted recordings (incoming calls, backgrounding)
  • Short, empty, or silent recordings
  • Storage limitations
  • Network failures during upload
  • Permission revocations

Automate testing with tools like Jest and Detox, and log errors for diagnostics.

Alternative Libraries and Workflows

While Expo’s managed workflow and expo-av suffice for many use cases, consider alternatives if you need:

  • Advanced background recording
  • Non-standard audio formats
  • Low-level audio processing

Alternatives include:

  • react-native-audio-recorder-player (bare workflow, more control)
  • react-native-sound
  • Custom native modules

Choose based on your app’s requirements and your willingness to handle native configuration.

Key Takeaways

Building a reliable, user-friendly audio recorder with Expo and React Native is achievable with careful attention to details beyond basic recording. Leverage expo-av for rapid development, but plan for production needs:

  • Always manage permissions and inform users.
  • Use staysActiveInBackground for best-effort background support.
  • Organize files for persistence and easy management.
  • Implement robust upload patterns, especially for unreliable networks.
  • Handle edge cases with grace.
  • Evaluate alternative libraries if you need deeper background or format support.

With these patterns, you’ll be able to deliver a polished, production-ready mobile audio app—whether you’re building voice notes, interviews, or creative audio tools.

Top comments (0)