DEV Community

Farouq Seriki
Farouq Seriki

Posted on

๐ŸŽฅ Mastering Media Uploads in React Native โ€” Images, Videos & Smart Compression (2026 Guide)

If youโ€™ve ever built a media-heavy app, you know the struggle.

A user snaps a gorgeous 4K video or a 12MP selfie and โ€” boom โ€” your upload queue explodes, your backend times out, and someone tweets,

โ€œYour app just crashed ๐Ÿ˜ญโ€

Between different camera APIs, resolutions, and codecs, file sizes can jump from a few KB to hundreds of MB.

And simply compressing once often isnโ€™t enough.

๐Ÿ’ก The smarter fix:

Set strict upload limits (e.g., โ‰ค 2 MB for images, โ‰ค 20 MB for video) and iteratively compress each file until it fits the cap โ€” all on-device.

Add good UX (progress bars, cancel buttons, smart defaults) and youโ€™ll deliver silky-smooth uploads even on slow connections.

๐Ÿงญ Table of Contents

  1. Why Compression Matters
  2. Quick Reference: Library Compatibility
  3. React Native CLI Setup (Images & Video)
  4. Expo Setup
  5. Full Image Compression & Upload Example
  6. Full Video Compression & Upload Example
  7. Iterative Compression Pattern
  8. Permissions & Troubleshooting
  9. Production Tips
  10. Final Thoughts

๐ŸŽฏ Why Compression Matters

  • Slow uploads & timeouts: mobile networks drop large requests.
  • Data cost: users burn bandwidth fast.
  • Server load: big files inflate storage and CDN bills.
  • UX: decoding large media tanks memory and drains battery.

Goal: compress client-side until it fits the backendโ€™s file-size threshold.

โšก Quick Reference: Library Compatibility

Library Min React Native Version Notes
react-native-video v6 โ‰ฅ 0.68.2 (RN 0.73 uses iOS 13+) iOS โ‰ฅ 13.0 required from 6.0.0-beta.8
react-native-fs โ‰ฅ 0.61 โ†’ use @2.16.0+ Native file access & utilities
react-native-image-picker Works โ‰ฅ 0.60 Handles photos & videos
react-native-compressor Works โ‰ฅ 0.65 Local media compression

โš™๏ธ React Native CLI Setup (Images & Video)

Weโ€™ll use:

Package Purpose Docs
react-native-image-picker Pick or capture photos & videos
react-native-compressor Compress images/videos efficiently
react-native-fs File size utilities & uploads
react-native-video Full-control video player

๐Ÿงฉ Installation

npx @react-native-community/cli@latest init MediaUploadRN
cd MediaUploadRN

npm install react-native-image-picker react-native-compressor react-native-fs react-native-video
cd ios && pod install && cd ..
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ react-native-fs Version Compatibility

React Native Version Install This Version
< 0.57 / Gradle < 3 react-native-fs@2.11.17
โ‰ฅ 0.57 / Gradle โ‰ฅ 3 react-native-fs@2.13.2+
โ‰ฅ 0.61 โœ… react-native-fs@2.16.0+
npm install react-native-fs@2.16.0
# RN < 0.60 only:
npx react-native link react-native-fs
Enter fullscreen mode Exit fullscreen mode

CocoaPods (iOS):

pod 'RNFS', :path => '../node_modules/react-native-fs'
pod install
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฌ React Native Video (v6.0.0+) Setup

โš ๏ธ V6.0.0 Information

  • Version 6 introduces modern architecture & performance improvements.
  • Requires React Native โ‰ฅ 0.68.2
  • From 6.0.0-beta.8 โ†’ requires iOS โ‰ฅ 13.0 (default in RN 0.73).
  • For older RN projects, stick with react-native-video@5.2.x.

๐Ÿ“ฆ Installation

npm install react-native-video
# or
yarn add react-native-video
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“ฑ iOS Setup

cd ios && pod install && cd ..
Enter fullscreen mode Exit fullscreen mode

Minimum: iOS 13 | RN 0.68.2+

Enable Custom Features (Podfile)

Video Caching:

$RNVideoUseVideoCaching=true
Enter fullscreen mode Exit fullscreen mode

Google IMA (ads):

$RNVideoUseGoogleIMA=true
Enter fullscreen mode Exit fullscreen mode

If caching is enabled, also add to Gemfile:

gem "cocoapods-swift-modular-headers"
Enter fullscreen mode Exit fullscreen mode

and run:

bundle install
bundle exec pod install
Enter fullscreen mode Exit fullscreen mode

๐Ÿค– Android Setup

Your project must use Kotlin โ‰ฅ 1.8 and compileSdk 34:

buildscript {
    ext.kotlinVersion = '1.8.0'
    ext.compileSdkVersion = 34
    ext.targetSdkVersion = 34
}
Enter fullscreen mode Exit fullscreen mode

Optional ExoPlayer flags (android/build.gradle):

buildscript {
  ext {
    useExoplayerHls = true
    useExoplayerDash = true
    useExoplayerSmoothStreaming = true
    useExoplayerIMA = false // enable if ads needed
    useExoplayerRtsp = false
  }
}
Enter fullscreen mode Exit fullscreen mode

โœ… Default enabled: SmoothStreaming, Dash, HLS
โš ๏ธ Each feature adds size โ€” enable only what you need.


๐Ÿ“ฑ Android Permissions

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- For older SDKs -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
Enter fullscreen mode Exit fullscreen mode

๐ŸŽ iOS Permissions

<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your library to select media.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to capture media.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for video audio.</string>
Enter fullscreen mode Exit fullscreen mode

๐Ÿ–ผ React Native Cli Image Compression & Upload Example

import React, { useState } from "react";
import {
  View,
  Button,
  Image,
  Alert,
  ScrollView,
  StyleSheet,
} from "react-native";
import * as ImagePicker from "react-native-image-picker";
import { Image as ImageCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";

const IMAGE_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
const UPLOAD_URL = "https://your.api/uploads";

async function getFileSize(uri: string) {
  const stat = await RNFS.stat(uri.replace("file://", ""));
  return Number(stat.size || 0);
}

async function compressImageUntilUnder(uri: string, maxBytes: number) {
  let quality = 0.8;
  let currentUri = uri;
  for (let i = 0; i < 5; i++) {
    const compressed = await ImageCompressor.compress(currentUri, {
      compressionMethod: "auto",
      quality,
    });
    const size = await getFileSize(compressed);
    if (size <= maxBytes) return compressed;
    quality = Math.max(0.3, quality - 0.15);
    currentUri = compressed;
  }
  return currentUri;
}

async function uploadFile(uri: string) {
  const form = new FormData();
  form.append("image", {
    // @ts-ignore
    uri,
    name: "upload.jpg",
    type: "image/jpeg",
  });
  await fetch(UPLOAD_URL, { method: "POST", body: form });
}

export default function ImageUploader() {
  const [uri, setUri] = useState<string | null>(null);
  const [compressedUri, setCompressedUri] = useState<string | null>(null);

  const pickImage = async () => {
    const res = await ImagePicker.launchImageLibrary({ mediaType: "photo" });
    if (res.didCancel || !res.assets) return;
    setUri(res.assets[0].uri ?? null);
  };

  const handleUpload = async () => {
    if (!uri) return Alert.alert("Pick an image first");
    const compressed = await compressImageUntilUnder(uri, IMAGE_MAX_BYTES);
    setCompressedUri(compressed);
    await uploadFile(compressed);
    Alert.alert("Upload complete!");
  };

  return (
    <ScrollView contentContainerStyle={styles.container}>
      <Button title="Pick Image" onPress={pickImage} />
      {uri && <Image source={{ uri }} style={styles.img} />}
      <Button title="Compress & Upload" onPress={handleUpload} />
      {compressedUri && (
        <Image source={{ uri: compressedUri }} style={styles.img} />
      )}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { padding: 20, gap: 16 },
  img: { height: 250, resizeMode: "contain", backgroundColor: "#eee" },
});
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฅ React Native Cli Video Compression & Upload Example

import React, { useState } from "react";
import { View, Button, Text, Alert, StyleSheet } from "react-native";
import * as ImagePicker from "react-native-image-picker";
import { Video as VideoCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";
import Video from "react-native-video";

const VIDEO_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
const UPLOAD_URL = "https://your.api/uploads";

async function getFileSize(uri: string) {
  const stat = await RNFS.stat(uri.replace("file://", ""));
  return Number(stat.size || 0);
}

async function compressVideoUntilUnder(
  uri: string,
  maxBytes: number,
  onProgress?: (p: number) => void
) {
  const presets: Array<"auto" | "medium" | "low"> = ["auto", "medium", "low"];
  let currentUri = uri;
  for (const preset of presets) {
    const compressed = await VideoCompressor.compress(
      currentUri,
      { compressionMethod: preset },
      (p) => onProgress?.(Math.round(p * 100))
    );
    const size = await getFileSize(compressed);
    if (size <= maxBytes) return compressed;
    currentUri = compressed;
  }
  return currentUri;
}

async function uploadVideo(uri: string) {
  const form = new FormData();
  form.append("video", {
    // @ts-ignore
    uri,
    name: "video.mp4",
    type: "video/mp4",
  });
  await fetch(UPLOAD_URL, { method: "POST", body: form });
}

export default function VideoUploader() {
  const [uri, setUri] = useState<string | null>(null);
  const [compressedUri, setCompressedUri] = useState<string | null>(null);
  const [progress, setProgress] = useState<number | null>(null);

  const pickVideo = async () => {
    const res = await ImagePicker.launchImageLibrary({ mediaType: "video" });
    if (res.didCancel || !res.assets) return;
    setUri(res.assets[0].uri ?? null);
  };

  const handleUpload = async () => {
    if (!uri) return Alert.alert("Pick a video first");
    const compressed = await compressVideoUntilUnder(
      uri,
      VIDEO_MAX_BYTES,
      setProgress
    );
    setCompressedUri(compressed);
    await uploadVideo(compressed);
    Alert.alert("Upload complete!");
    setProgress(null);
  };

  return (
    <View style={styles.container}>
      <Button title="Pick Video" onPress={pickVideo} />
      {progress != null && <Text>Compressing: {progress}%</Text>}
      <Button
        title="Compress & Upload"
        onPress={handleUpload}
        disabled={progress != null}
      />
      {compressedUri && (
        <Video
          source={{ uri: compressedUri }}
          style={styles.video}
          controls
          paused
        />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { padding: 20, gap: 16 },
  video: { height: 220, backgroundColor: "#000" },
});
Enter fullscreen mode Exit fullscreen mode

โš™๏ธ Expo Setup โ€” Capture, Compress & Upload Media

Expo makes working with media surprisingly straightforward โ€” whether youโ€™re handling simple image uploads or full video workflows.
You can choose between two reliable approaches depending on your project type:

  1. ๐Ÿ–ผ Image Compression Only (Pure Expo) โ€” works entirely inside the Managed Workflow using expo-image-manipulator.
  2. ๐ŸŽฅ Video Compression (Dev Build Mode) โ€” unlocks native-level compression and playback with react-native-compressor, react-native-fs, and react-native-video.

Both methods deliver efficient, on-device compression โ€” no backend preprocessing required.


๐Ÿ–ผ Image Compression Only โ€” Expo Image Manipulator

If youโ€™re building with Pure Expo (Managed Workflow), you can compress and resize images directly on the device using expo-image-manipulator.
This library is fast, stable, and ideal for reducing image size before uploads โ€” perfect for profile photos, product images, or gallery uploads.


โš™๏ธ Installation

npx expo install expo-image-manipulator expo-file-system expo-image-picker
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ป Example โ€” Dynamic Image Compression & Resizing

The example below dynamically compresses any selected image to stay under a custom file-size limit (e.g., 2 MB, 5 MB, 10 MB).
It progressively reduces quality and resolution until the image meets the target threshold โ€” all on-device.

import React, { useEffect, useState } from "react";
import {
  View,
  Text,
  StyleSheet,
  Button,
  ActivityIndicator,
  Image,
  Alert,
} from "react-native";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
import { useImageManipulator, SaveFormat } from "expo-image-manipulator";

const DEFAULT_COMPRESSION_QUALITY = 0.8;
const MIN_QUALITY = 0.3;
const DOWNSCALE_FACTOR = 0.85;

// ---------- File Size Utilities ----------
const getFileSize = async (uri: string): Promise<number> => {
  const info = await FileSystem.getInfoAsync(uri);
  return info.size || 0;
};

const formatFileSize = (bytes: number) =>
  `${(bytes / 1024 / 1024).toFixed(2)} MB`;

// ---------- Dynamic Compression Logic ----------
const compressImageUntilUnder = async (
  uri: string,
  manipulator: ReturnType<typeof useImageManipulator>,
  maxSizeMB: number
): Promise<string> => {
  try {
    const MAX_FILE_SIZE = maxSizeMB * 1024 * 1024;
    let quality = DEFAULT_COMPRESSION_QUALITY;
    let currentUri = uri;
    let currentSize = await getFileSize(uri);
    let width: number | undefined;
    let height: number | undefined;

    console.log(
      `๐Ÿ“ธ Starting compression: ${formatFileSize(
        currentSize
      )} (limit ${maxSizeMB} MB)`
    );

    while (currentSize > MAX_FILE_SIZE && quality > MIN_QUALITY) {
      if (width || height) manipulator.resize({ width, height });
      const rendered = await manipulator.renderAsync();
      const compressed = await rendered.saveAsync({
        compress: quality,
        format: SaveFormat.JPEG,
      });

      const newSize = await getFileSize(compressed.uri);
      console.log(`โ†’ ${formatFileSize(newSize)} @ quality ${quality}`);

      if (newSize > MAX_FILE_SIZE) {
        quality *= 0.8;
        width = (rendered.width || 0) * DOWNSCALE_FACTOR;
        height = (rendered.height || 0) * DOWNSCALE_FACTOR;
        currentUri = compressed.uri;
        currentSize = newSize;
      } else {
        console.log(`โœ… Final size under ${maxSizeMB} MB`);
        return compressed.uri;
      }
    }

    console.warn(
      `โš ๏ธ Could not compress below ${maxSizeMB} MB โ€” returning best result.`
    );
    return currentUri;
  } catch (err) {
    console.error("Compression error:", err);
    return uri;
  }
};

// ---------- Compression Overlay ----------
const ImageCompressor = ({
  uri,
  onComplete,
  maxSizeMB,
}: {
  uri: string;
  onComplete: (resultUri: string) => void;
  maxSizeMB: number;
}) => {
  const manipulator = useImageManipulator(uri);
  const [isCompressing, setIsCompressing] = useState(false);

  useEffect(() => {
    const compress = async () => {
      setIsCompressing(true);
      try {
        const originalSize = await getFileSize(uri);
        console.log("Original size:", formatFileSize(originalSize));

        const rendered = await manipulator.renderAsync();
        const temp = await rendered.saveAsync({
          compress: DEFAULT_COMPRESSION_QUALITY,
          format: SaveFormat.JPEG,
        });

        const firstPassSize = await getFileSize(temp.uri);
        let finalUri = temp.uri;

        if (firstPassSize > maxSizeMB * 1024 * 1024) {
          finalUri = await compressImageUntilUnder(
            temp.uri,
            manipulator,
            maxSizeMB
          );
        }

        const finalSize = await getFileSize(finalUri);
        console.log("โœ… Final compressed size:", formatFileSize(finalSize));

        onComplete(finalUri);
      } catch (e) {
        console.error(e);
        onComplete(uri);
      } finally {
        setIsCompressing(false);
      }
    };
    compress();
  }, [uri]);

  if (!isCompressing) return null;

  return (
    <View style={styles.overlay}>
      <ActivityIndicator size="large" color="#0F6B2D" />
      <Text style={styles.overlayText}>Compressing image...</Text>
    </View>
  );
};

// ---------- Main Component ----------
export default function ExpoImageCompressor() {
  const [imageUri, setImageUri] = useState<string | null>(null);
  const [finalUri, setFinalUri] = useState<string | null>(null);
  const [maxSizeMB, setMaxSizeMB] = useState<number>(2); // dynamic user limit

  const pickImage = async () => {
    const res = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
      allowsEditing: true,
      quality: 1,
    });
    if (!res.canceled) {
      setImageUri(res.assets[0].uri);
    }
  };

  return (
    <View style={styles.container}>
      <Button title="Pick Image" onPress={pickImage} />
      {imageUri && <Image source={{ uri: imageUri }} style={styles.img} />}
      <Text style={styles.label}>Max Size: {maxSizeMB} MB</Text>

      {imageUri && (
        <ImageCompressor
          uri={imageUri}
          maxSizeMB={maxSizeMB}
          onComplete={(uri) => {
            setFinalUri(uri);
            Alert.alert("โœ… Compression complete!");
          }}
        />
      )}

      {finalUri && <Image source={{ uri: finalUri }} style={styles.img} />}
    </View>
  );
}

// ---------- Styles ----------
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    padding: 20,
  },
  img: { width: "90%", height: 250, marginTop: 20, borderRadius: 12 },
  label: { marginTop: 16, fontSize: 16, color: "#333" },
  overlay: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: "rgba(0,0,0,0.7)",
    justifyContent: "center",
    alignItems: "center",
  },
  overlayText: { color: "white", marginTop: 12 },
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Why It Works

  • Dynamic Limit: Easily change the size cap (maxSizeMB) for different use cases (e.g., 2 MB for avatars, 10 MB for photos).
  • Iterative Compression: Reduces both quality and resolution in steps until under the defined limit.
  • Expo-Only: Runs fully in JavaScript โ€” no native linking or prebuild required.
  • Real-World Safe: Handles large images gracefully and provides user feedback with an activity overlay.
  • Reusable: Drop into any project โ€” just pass a maxSizeMB value and a completion callback.

๐ŸŽฅ Video Compression โ€” Dev Build Mode

If your app needs to handle video uploads or large media files, switch to an Expo Dev Build.
This mode enables access to native libraries for true on-device compression, caching, and playback.


โš™๏ธ Installation

npm install react-native-compressor react-native-fs react-native-video
npx expo prebuild
npx expo run:android
# or
npx expo run:ios
Enter fullscreen mode Exit fullscreen mode

Once complete, your project will have native access to efficient compression and smooth video rendering.


๐Ÿ’ป Example โ€” Dynamic Video Compression Helper

import { Video as VideoCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";

/**
 * Compresses a video dynamically until it fits under the given size (in MB).
 * Supports both Expo Dev Build and React Native CLI setups.
 */
export async function compressVideoUntilUnder(
  uri: string,
  maxSizeMB: number,
  onProgress?: (p: number) => void
): Promise<string> {
  try {
    const maxBytes = maxSizeMB * 1024 * 1024;
    const presets: Array<"auto" | "medium" | "low"> = ["auto", "medium", "low"];
    let currentUri = uri;

    const getFileSize = async (fileUri: string): Promise<number> => {
      const stat = await RNFS.stat(fileUri.replace("file://", ""));
      return Number(stat.size || 0);
    };

    let currentSize = await getFileSize(currentUri);
    console.log(
      `๐ŸŽฌ Original size: ${(currentSize / 1024 / 1024).toFixed(2)} MB`
    );

    for (const preset of presets) {
      console.log(`โš™๏ธ Trying preset: ${preset}`);
      const compressed = await VideoCompressor.compress(
        currentUri,
        { compressionMethod: preset },
        (progress) => onProgress?.(Math.round(progress * 100))
      );

      const newSize = await getFileSize(compressed);
      console.log(`โ†’ ${formatSize(newSize)} after ${preset}`);

      if (newSize <= maxBytes) {
        console.log(`โœ… Final size under ${maxSizeMB} MB`);
        return compressed;
      }

      currentUri = compressed;
      currentSize = newSize;
    }

    console.warn(
      `โš ๏ธ Could not compress below ${maxSizeMB} MB โ€” returning best result.`
    );
    return currentUri;
  } catch (err) {
    console.error("โŒ Compression failed:", err);
    return uri;
  }
}

const formatSize = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
Enter fullscreen mode Exit fullscreen mode

โœ… What You Unlock

Library Purpose
react-native-compressor Efficient on-device video and image compression
react-native-fs File-size inspection, local reads/writes, and uploads
react-native-video Smooth playback, caching, and adaptive streaming

๐Ÿ“˜ Note: Expo Dev Builds fully support config plugins, allowing features like caching and streaming exactly like a bare React Native project.


๐Ÿงฉ Summary

  • ๐Ÿ–ผ Pure Expo (Managed Workflow): Use expo-image-manipulator for image compression โ€” 100% JavaScript, no native build needed.
  • ๐ŸŽฅ Dev Build Mode (Native): Use react-native-compressor and react-native-video for professional-grade video compression and playback.

Both approaches help you deliver faster uploads, smaller files, and better mobile UX โ€” all while staying true to Expoโ€™s developer-friendly workflow.

๐Ÿ” Iterative Compression Pattern

while (size(uri) > limit && attempts < MAX_ATTEMPTS) {
  uri = compress(uri, nextParams());
}
Enter fullscreen mode Exit fullscreen mode

Ensures consistent output sizes across devices.


๐Ÿงฐ Permissions & Troubleshooting

  • Android 13+: READ_MEDIA_IMAGES, READ_MEDIA_VIDEO
  • Older Android: READ_EXTERNAL_STORAGE
  • iOS: NSCameraUsageDescription, NSPhotoLibraryUsageDescription, NSMicrophoneUsageDescription
  • Always check file size after compression โ€” Android content:// URIs can differ.
  • Test on real devices; emulators lack cameras and true codecs.

๐Ÿ— Production Tips

  • Enforce size limits in the backend too.
  • Resize before compressing (e.g., 1920 px width cap).
  • Adjust compression by connection type (Wi-Fi vs cellular).
  • Generate thumbnails for video feeds.
  • Keep compression async โ€” never block the UI.

โœ… Final Thoughts

Compression isnโ€™t just a backend optimization โ€” itโ€™s a user-experience feature.
By iteratively compressing files, tracking progress, and configuring each library correctly (especially React Native Video v6 and react-native-fs), youโ€™ll keep uploads fast, consistent, and reliable across every device.

๐Ÿ’ฌ If this guide helped, drop a โค๏ธ or share your own setup for React Native media uploads.

Top comments (0)