This comparison evaluates five critical libraries for managing media assets, file system operations, and device permissions in React Native applications. expo-media-library provides a managed, high-level API for saving and accessing media within the Expo ecosystem. react-native-camera is a legacy library for camera capture that has been officially deprecated in favor of expo-camera or react-native-vision-camera. react-native-fs offers low-level bridging to native file system APIs for reading, writing, and moving files. react-native-image-picker simplifies the complex task of selecting images and videos from the device's gallery with a unified JavaScript interface. Finally, react-native-permissions acts as a unified layer to request and check status for iOS and Android permissions, which is a prerequisite for using any of the other libraries effectively.
Building robust mobile applications often requires interacting with the device's hardware and storage. You need to take photos, save them to the gallery, read files from the disk, and ask users for permission to do so. The React Native ecosystem offers several tools for these tasks, but they serve very different purposes and vary greatly in their maintenance status. Let's break down how expo-media-library, react-native-camera, react-native-fs, react-native-image-picker, and react-native-permissions fit into your architecture.
Before writing any code, you must address the elephant in the room. react-native-camera is deprecated.
The maintainers have officially archived the repository. It no longer receives updates for new iOS or Android versions, meaning it will likely crash on modern devices or fail to build with recent React Native versions. Using it in a new project introduces significant technical debt immediately.
// β DO NOT USE: react-native-camera is deprecated
import { RNCamera } from 'react-native-camera';
// β
USE INSTEAD: expo-camera (for Expo) or react-native-vision-camera (for Bare)
import { Camera } from 'expo-camera';
// or
import { Camera } from 'react-native-vision-camera';
If you encounter this package in a legacy codebase, your first architectural task should be planning its replacement. For the rest of this comparison, we will treat it as a legacy component to avoid, focusing on the active alternatives.
A common requirement is getting an image from the user. You have two main paths: building a custom camera interface or using the device's built-in tools.
react-native-image-picker chooses the path of least resistance. It launches the native system UI for the gallery or the default camera app. You don't control the look and feel, but you get a reliable, native experience with zero custom UI code.
// react-native-image-picker: Launches system UI
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
const options = { mediaType: 'photo' };
// Opens system camera
const cameraResult = await launchCamera(options);
// Opens system gallery
const galleryResult = await launchImageLibrary(options);
if (cameraResult.assets) {
console.log(cameraResult.assets[0].uri);
}
expo-media-library (often paired with expo-camera) allows you to build a custom camera interface inside your app. This gives you full control over overlays, buttons, and scanning logic. Once you capture the image, expo-media-library is responsible for saving it to the user's public gallery.
// expo-media-library: Save a captured URI to the public gallery
import * as MediaLibrary from 'expo-media-library';
// Assume 'uri' comes from a custom camera component like expo-camera
const asset = await MediaLibrary.createAssetAsync(uri);
// Optionally add to a specific album
const album = await MediaLibrary.getAlbumAsync('My App Photos');
if (album) {
await MediaLibrary.addAssetsToAlbumAsync(asset, album, false);
} else {
await MediaLibrary.createAlbumAsync('My App Photos', asset, false);
}
Trade-off: Use react-native-image-picker for simple forms (like profile uploads). Use expo-media-library + a custom camera component if you are building a social app, a scanner, or anything requiring a branded camera experience.
Once you have an image or file, where does it live? This is where react-native-fs and expo-media-library diverge.
expo-media-library operates at a high level. It treats photos and videos as "assets." You don't worry about file paths like /var/mobile/.... You just ask to save an asset or fetch a list of assets from an album. It handles the complexity of iOS Photos libraries and Android MediaStore for you.
// expo-media-library: Querying assets without knowing file paths
import * as MediaLibrary from 'expo-media-library';
const fetchParams = {
first: 10,
album: 'Recent',
mediaType: MediaLibrary.MediaType.photo,
};
const media = await MediaLibrary.getAssetsAsync(fetchParams);
// media.assets contains objects with id, uri, filename, but abstracts the physical path
console.log(media.assets[0].filename);
react-native-fs drops you down to the metal. It gives you direct access to the file system hierarchy. You can read files, write binary data, create directories, and move files between sandboxed folders. This is crucial if you need to upload a file to a server as a blob, decrypt a local database, or manage documents outside the media gallery.
// react-native-fs: Direct file manipulation
import RNFS from 'react-native-fs';
const path = `${RNFS.DocumentDirectoryPath}/report.pdf`;
// Write a file
await RNFS.writeFile(path, 'Binary content here', 'utf8');
// Read a file
const contents = await RNFS.readFile(path, 'utf8');
// Move a file from cache to documents
await RNFS.moveFile(
`${RNFS.CachesDirectoryPath}/temp-image.jpg`,
`${RNFS.DocumentDirectoryPath}/saved-image.jpg`
);
Trade-off: If you are just saving user photos, stick to expo-media-library. If you are building a file manager, a document editor, or need to manipulate raw bytes, react-native-fs is mandatory.
None of the above libraries will work without user consent. iOS and Android have strict rules about accessing the camera, gallery, and file system. Handling these prompts incorrectly can get your app rejected or cause crashes.
react-native-permissions is the standard solution for this. It provides a unified API to check if permission is granted, denied, or blocked, and to trigger the native request dialog. It supports a vast matrix of permission types across both platforms.
// react-native-permissions: Unified permission handling
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
async function requestCameraAccess() {
// Check current status
const status = await check(PERMISSIONS.IOS.CAMERA);
if (status === RESULTS.DENIED) {
// Request permission
const result = await request(PERMISSIONS.IOS.CAMERA);
if (result === RESULTS.GRANTED) {
console.log('Camera access granted');
} else {
console.log('Camera access denied or blocked');
}
}
}
While expo-media-library has built-in permission helpers (like MediaLibrary.requestPermissionsAsync), they are tied to the Expo ecosystem. react-native-permissions is more flexible for bare workflows and covers edge cases (like "Always Allow" vs "Allow While Using App" for location) that specific media libraries might not expose fully.
In a production app, you rarely use just one of these. A typical "Upload Profile Picture" flow combines them.
react-native-permissions to ensure you can access the camera.react-native-image-picker to let the user choose a photo.react-native-fs to move the image from the temporary cache to a permanent directory if you need to edit it locally.expo-media-library to save it to their public albums.// Combined Workflow Example
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { launchImageLibrary } from 'react-native-image-picker';
import RNFS from 'react-native-fs';
import * as MediaLibrary from 'expo-media-library';
async function handleProfileUpload() {
// 1. Request Permission
const permStatus = await request(PERMISSIONS.IOS.PHOTO_LIBRARY);
if (permStatus !== RESULTS.GRANTED) return;
// 2. Pick Image
const result = await launchImageLibrary({ mediaType: 'photo' });
if (!result.assets || result.assets.length === 0) return;
const sourceUri = result.assets[0].uri;
// 3. Move File (if needed for processing)
const destPath = `${RNFS.DocumentDirectoryPath}/profile_temp.jpg`;
// Note: URI handling differs between iOS/Android, simplified here
await RNFS.copyFile(sourceUri.replace('file://', ''), destPath);
// 4. Save to Gallery (Optional user preference)
const shouldSave = true;
if (shouldSave) {
await MediaLibrary.createAssetAsync(destPath);
}
// Proceed to upload 'destPath' to server...
}
| Feature | expo-media-library | react-native-camera | react-native-fs | react-native-image-picker | react-native-permissions |
|---|---|---|---|---|---|
| Primary Goal | Save/Manage Gallery Assets | Capture Photos/Video (Legacy) | Raw File System Access | Select/Capture via System UI | Request Device Permissions |
| Maintenance | β Active (Expo Team) | β Deprecated | β Active | β Active | β Active |
| UI Control | None (API only) | Full Custom UI | None | System Default UI | System Default Dialogs |
| File Access | High-level (Assets) | N/A | Low-level (Paths/Binary) | High-level (URIs) | N/A |
| Best For | Social apps, saving photos | Do Not Use | File managers, offline data | Profile uploads, simple forms | All apps needing hardware access |
For modern React Native development, your stack should look like this:
react-native-permissions. It is the most robust way to handle the fragmented permission landscapes of iOS and Android.react-native-image-picker first. It solves 80% of use cases with minimal code.react-native-camera. Use expo-camera if you are in the Expo ecosystem, or react-native-vision-camera for bare projects.expo-media-library to interact with the user's photo roll. It abstracts the painful differences between iOS Photos and Android MediaStore.react-native-fs in your toolkit for when you need to move, read, or write files directly. Do not use it for simple gallery saves; use the higher-level tools instead.By combining these active, well-maintained libraries, you can build media features that are resilient, native-feeling, and ready for the app store review process.
Choose expo-media-library if you are building within the Expo Managed Workflow or want a stable, high-level API for saving photos/videos and accessing album metadata without writing native code. It is the best choice for standard gallery interactions where deep customization of the camera UI is not required.
Do NOT choose react-native-camera for new projects; it is officially deprecated and no longer maintained. If you are maintaining a legacy app using it, plan a migration to expo-camera (for Expo apps) or react-native-vision-camera (for bare React Native apps) to ensure security updates and compatibility with modern OS versions.
Choose react-native-fs when you need low-level control over the device's file system, such as downloading files to specific directories, reading binary data, or moving files outside of the media store. It is essential for apps that handle document management, offline caching, or complex file manipulation logic.
Choose react-native-image-picker if your primary goal is to let users select existing media from their gallery or quickly snap a photo using the system's default camera UI. It is the industry standard for simple 'upload profile picture' flows where building a custom camera interface is unnecessary overhead.
Choose react-native-permissions for any production app that requires robust handling of iOS and Android permission dialogs. It abstracts away the fragmented native permission APIs, providing a consistent way to check status, request access, and handle 'never ask again' scenarios before attempting to use camera or file system features.