File SystemStorageReact Native

React Native FS Complete Guide: Efficient File System Operations and Best Practices

By Viewlytics Team13 min readMay 29, 2025

File system operations are essential for many React Native applications. Whether you're downloading files, caching images, or managing local data, react-native-fs provides powerful APIs to handle file operations efficiently. This comprehensive guide covers everything you need to know about using react-native-fs effectively while avoiding common pitfalls.

1. Introduction to React Native FS

React Native FS (react-native-fs) is a native file system access library for React Native that provides a comprehensive set of APIs for file and directory operations. It supports both iOS and Android platforms, offering functionality for reading, writing, uploading, downloading, and managing files.

Key Features

  • Cross-platform support: Works on both iOS and Android
  • File operations: Read, write, copy, move, and delete files
  • Directory management: Create, list, and manage directories
  • Network operations: Download and upload files with progress tracking
  • Asset access: Read files from app bundles and resources
  • Permissions handling: Proper permissions management for external storage

Installation and Setup

Install react-native-fs using npm or yarn:

# Using npm
npm install react-native-fs --save

# Using yarn
yarn add react-native-fs

For React Native 0.61 and above, the library should auto-link. For older versions, you'll need to manually link:

# For RN < 0.61
react-native link react-native-fs

For Android, ensure you have the proper permissions in your AndroidManifest.xml:

<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

2. Directory Paths and Constants

Understanding the available directory paths is crucial for effective file management. React Native FS provides several predefined constants for different storage locations.

Available Directory Constants

import RNFS from 'react-native-fs';

// iOS and Android
const documentDirectory = RNFS.DocumentDirectoryPath;
const cachesDirectory = RNFS.CachesDirectoryPath;
const temporaryDirectory = RNFS.TemporaryDirectoryPath;

// iOS only
const mainBundlePath = RNFS.MainBundlePath;
const libraryDirectory = RNFS.LibraryDirectoryPath;

// Android only
const externalCachesDirectory = RNFS.ExternalCachesDirectoryPath;
const externalDirectory = RNFS.ExternalDirectoryPath;
const externalStorageDirectory = RNFS.ExternalStorageDirectoryPath;
const downloadDirectory = RNFS.DownloadDirectoryPath;

console.log('Document Directory:', documentDirectory);
console.log('Caches Directory:', cachesDirectory);

Choosing the Right Directory

Best Practice: Use DocumentDirectoryPath for user data that should persist, CachesDirectoryPath for temporary files that can be cleared, and TemporaryDirectoryPath for files that should be cleaned up automatically.
// Helper function to get appropriate directory
function getStorageDirectory(purpose: 'cache' | 'documents' | 'temp') {
  switch (purpose) {
    case 'cache':
      return RNFS.CachesDirectoryPath;
    case 'documents':
      return RNFS.DocumentDirectoryPath;
    case 'temp':
      return RNFS.TemporaryDirectoryPath;
    default:
      return RNFS.DocumentDirectoryPath;
  }
}

// Usage examples
const cacheDir = getStorageDirectory('cache');
const userDataDir = getStorageDirectory('documents');

3. Basic File Operations

Reading Files

React Native FS provides multiple methods for reading files, including full file reads and chunked reads for better memory management.

// Basic file reading
async function readFileContent(filePath: string): Promise<string> {
  try {
    const content = await RNFS.readFile(filePath, 'utf8');
    return content;
  } catch (error) {
    console.error('Error reading file:', error);
    throw error;
  }
}

// Reading binary files (images, PDFs, etc.)
async function readBinaryFile(filePath: string): Promise<string> {
  try {
    const base64Content = await RNFS.readFile(filePath, 'base64');
    return base64Content;
  } catch (error) {
    console.error('Error reading binary file:', error);
    throw error;
  }
}

// Reading files in chunks (for large files)
async function readFileInChunks(filePath: string, chunkSize: number = 1024): Promise<string[]> {
  try {
    const stat = await RNFS.stat(filePath);
    const fileSize = stat.size;
    const chunks: string[] = [];
    
    for (let position = 0; position < fileSize; position += chunkSize) {
      const chunk = await RNFS.read(filePath, chunkSize, position, 'utf8');
      chunks.push(chunk);
    }
    
    return chunks;
  } catch (error) {
    console.error('Error reading file in chunks:', error);
    throw error;
  }
}

Writing Files

File writing operations should be handled carefully to avoid data corruption and ensure proper error handling.

// Basic file writing
async function writeFile(filePath: string, content: string): Promise<void> {
  try {
    await RNFS.writeFile(filePath, content, 'utf8');
    console.log('File written successfully');
  } catch (error) {
    console.error('Error writing file:', error);
    throw error;
  }
}

// Atomic file writing (safer for critical data)
async function writeFileAtomic(filePath: string, content: string): Promise<void> {
  const tempPath = `${filePath}.tmp`;
  
  try {
    // Write to temporary file first
    await RNFS.writeFile(tempPath, content, 'utf8');
    
    // Move temp file to final location (atomic operation)
    await RNFS.moveFile(tempPath, filePath);
    
    console.log('File written atomically');
  } catch (error) {
    // Clean up temp file if it exists
    const tempExists = await RNFS.exists(tempPath);
    if (tempExists) {
      await RNFS.unlink(tempPath);
    }
    
    console.error('Error writing file atomically:', error);
    throw error;
  }
}

// Appending to files
async function appendToFile(filePath: string, content: string): Promise<void> {
  try {
    await RNFS.appendFile(filePath, content, 'utf8');
    console.log('Content appended successfully');
  } catch (error) {
    console.error('Error appending to file:', error);
    throw error;
  }
}

File and Directory Management

// Create directory with proper error handling
async function createDirectory(dirPath: string): Promise<void> {
  try {
    const exists = await RNFS.exists(dirPath);
    if (!exists) {
      await RNFS.mkdir(dirPath);
      console.log('Directory created:', dirPath);
    }
  } catch (error) {
    console.error('Error creating directory:', error);
    throw error;
  }
}

// List directory contents with filtering
async function listDirectory(dirPath: string, filter?: string): Promise<RNFS.ReadDirItem[]> {
  try {
    const items = await RNFS.readDir(dirPath);
    
    if (filter) {
      return items.filter(item => item.name.includes(filter));
    }
    
    return items;
  } catch (error) {
    console.error('Error listing directory:', error);
    throw error;
  }
}

// Copy files with progress tracking
async function copyFileWithProgress(
  source: string, 
  destination: string,
  onProgress?: (progress: number) => void
): Promise<void> {
  try {
    const stat = await RNFS.stat(source);
    const totalSize = stat.size;
    
    if (onProgress) {
      onProgress(0);
    }
    
    await RNFS.copyFile(source, destination);
    
    if (onProgress) {
      onProgress(100);
    }
    
    console.log('File copied successfully');
  } catch (error) {
    console.error('Error copying file:', error);
    throw error;
  }
}

// Safe file deletion
async function deleteFile(filePath: string): Promise<void> {
  try {
    const exists = await RNFS.exists(filePath);
    if (exists) {
      await RNFS.unlink(filePath);
      console.log('File deleted:', filePath);
    }
  } catch (error) {
    console.error('Error deleting file:', error);
    throw error;
  }
}

4. Advanced File Operations

File Downloads with Progress Tracking

Downloading files efficiently with proper progress tracking and error handling is crucial for user experience.

type DownloadProgress = {
  jobId: number;
  contentLength: number;
  bytesWritten: number;
  percentage: number;
};

type DownloadOptions = {
  fromUrl: string;
  toFile: string;
  headers?: Record<string, string>;
  onProgress?: (progress: DownloadProgress) => void;
  onBegin?: (statusCode: number, contentLength: number) => void;
};

class FileDownloader {
  private downloadJobs: Map<number, RNFS.DownloadPromise> = new Map();

  async downloadFile(options: DownloadOptions): Promise<RNFS.DownloadResult> {
    const { fromUrl, toFile, headers = {}, onProgress, onBegin } = options;

    try {
      // Ensure destination directory exists
      const destDir = toFile.substring(0, toFile.lastIndexOf('/'));
      await RNFS.mkdir(destDir);

      const downloadOptions: RNFS.DownloadFileOptions = {
        fromUrl,
        toFile,
        headers,
        background: false,
        discretionary: true,
        begin: (res) => {
          console.log('Download started:', res.statusCode, res.contentLength);
          onBegin?.(res.statusCode, res.contentLength);
        },
        progress: (res) => {
          const percentage = Math.floor((res.bytesWritten / res.contentLength) * 100);
          onProgress?.({
            jobId: res.jobId,
            contentLength: res.contentLength,
            bytesWritten: res.bytesWritten,
            percentage,
          });
        },
      };

      const downloadPromise = RNFS.downloadFile(downloadOptions);
      this.downloadJobs.set(downloadPromise.jobId, downloadPromise);

      const result = await downloadPromise.promise;
      this.downloadJobs.delete(downloadPromise.jobId);

      return result;
    } catch (error) {
      console.error('Download failed:', error);
      throw error;
    }
  }

  stopDownload(jobId: number): void {
    const downloadJob = this.downloadJobs.get(jobId);
    if (downloadJob) {
      RNFS.stopDownload(jobId);
      this.downloadJobs.delete(jobId);
    }
  }

  stopAllDownloads(): void {
    this.downloadJobs.forEach((_, jobId) => {
      RNFS.stopDownload(jobId);
    });
    this.downloadJobs.clear();
  }
}

// Usage example
const downloader = new FileDownloader();

async function downloadImage(url: string, filename: string): Promise<string> {
  const filePath = `${RNFS.DocumentDirectoryPath}/${filename}`;
  
  try {
    const result = await downloader.downloadFile({
      fromUrl: url,
      toFile: filePath,
      onProgress: (progress) => {
        console.log(`Download progress: ${progress.percentage}%`);
      },
      onBegin: (statusCode, contentLength) => {
        console.log(`Download started: ${statusCode}, Size: ${contentLength} bytes`);
      },
    });

    if (result.statusCode === 200) {
      return filePath;
    } else {
      throw new Error(`Download failed with status code: ${result.statusCode}`);
    }
  } catch (error) {
    console.error('Failed to download image:', error);
    throw error;
  }
}

File Uploads

type UploadProgress = {
  totalBytesSent: number;
  totalBytesExpectedToSend: number;
  percentage: number;
};

type UploadOptions = {
  toUrl: string;
  files: RNFS.UploadFileItem[];
  headers?: Record<string, string>;
  fields?: Record<string, string>;
  method?: 'POST' | 'PUT';
  onProgress?: (progress: UploadProgress) => void;
};

class FileUploader {
  async uploadFiles(options: UploadOptions): Promise<RNFS.UploadResult> {
    const { toUrl, files, headers = {}, fields = {}, method = 'POST', onProgress } = options;

    try {
      const uploadOptions: RNFS.UploadFileOptions = {
        toUrl,
        files,
        headers,
        fields,
        method,
        begin: (response) => {
          console.log('Upload started, jobId:', response.jobId);
        },
        progress: (response) => {
          const percentage = Math.floor(
            (response.totalBytesSent / response.totalBytesExpectedToSend) * 100
          );
          
          onProgress?.({
            totalBytesSent: response.totalBytesSent,
            totalBytesExpectedToSend: response.totalBytesExpectedToSend,
            percentage,
          });
        },
      };

      const uploadPromise = RNFS.uploadFiles(uploadOptions);
      const result = await uploadPromise.promise;

      return result;
    } catch (error) {
      console.error('Upload failed:', error);
      throw error;
    }
  }
}

// Usage example
async function uploadProfileImage(imagePath: string, userId: string): Promise<void> {
  const uploader = new FileUploader();
  
  const files: RNFS.UploadFileItem[] = [
    {
      name: 'profile_image',
      filename: 'profile.jpg',
      filepath: imagePath,
      filetype: 'image/jpeg',
    },
  ];

  try {
    const result = await uploader.uploadFiles({
      toUrl: 'https://api.example.com/upload',
      files,
      headers: {
        'Authorization': `Bearer ${getAuthToken()}`,
      },
      fields: {
        userId,
      },
      onProgress: (progress) => {
        console.log(`Upload progress: ${progress.percentage}%`);
      },
    });

    if (result.statusCode === 200) {
      console.log('Upload successful:', result.body);
    } else {
      throw new Error(`Upload failed with status code: ${result.statusCode}`);
    }
  } catch (error) {
    console.error('Failed to upload image:', error);
    throw error;
  }
}

5. Performance Optimization

Memory Management for Large Files

When dealing with large files, proper memory management is crucial to prevent out-of-memory errors.

// Efficient large file processing
class LargeFileHandler {
  private readonly CHUNK_SIZE = 1024 * 1024; // 1MB chunks

  async processLargeFile(
    filePath: string,
    processor: (chunk: string, index: number) => Promise<void>
  ): Promise<void> {
    try {
      const stat = await RNFS.stat(filePath);
      const fileSize = stat.size;
      let position = 0;
      let chunkIndex = 0;

      while (position < fileSize) {
        const remainingSize = fileSize - position;
        const chunkSize = Math.min(this.CHUNK_SIZE, remainingSize);
        
        const chunk = await RNFS.read(filePath, chunkSize, position, 'utf8');
        await processor(chunk, chunkIndex);
        
        position += chunkSize;
        chunkIndex++;
        
        // Allow garbage collection between chunks
        await new Promise(resolve => setImmediate(resolve));
      }
    } catch (error) {
      console.error('Error processing large file:', error);
      throw error;
    }
  }

  async streamFileUpload(
    filePath: string,
    uploadUrl: string,
    chunkSize: number = this.CHUNK_SIZE
  ): Promise<void> {
    try {
      const stat = await RNFS.stat(filePath);
      const fileSize = stat.size;
      const totalChunks = Math.ceil(fileSize / chunkSize);
      
      for (let i = 0; i < totalChunks; i++) {
        const start = i * chunkSize;
        const end = Math.min(start + chunkSize, fileSize);
        const chunk = await RNFS.read(filePath, end - start, start, 'base64');
        
        await this.uploadChunk(uploadUrl, chunk, i, totalChunks);
        
        console.log(`Uploaded chunk ${i + 1}/${totalChunks}`);
      }
    } catch (error) {
      console.error('Error streaming file upload:', error);
      throw error;
    }
  }

  private async uploadChunk(
    url: string,
    chunk: string,
    index: number,
    total: number
  ): Promise<void> {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Chunk-Index': index.toString(),
        'X-Total-Chunks': total.toString(),
      },
      body: JSON.stringify({ chunk, index, total }),
    });

    if (!response.ok) {
      throw new Error(`Chunk upload failed: ${response.statusText}`);
    }
  }
}

Caching Strategies

type CacheEntry = {
  filePath: string;
  lastAccessed: number;
  size: number;
};

class FileCache {
  private readonly maxCacheSize: number;
  private readonly cacheDir: string;
  private cacheIndex: Map<string, CacheEntry> = new Map();

  constructor(maxSizeMB: number = 100) {
    this.maxCacheSize = maxSizeMB * 1024 * 1024; // Convert to bytes
    this.cacheDir = `${RNFS.CachesDirectoryPath}/file_cache`;
    this.initializeCache();
  }

  private async initializeCache(): Promise<void> {
    try {
      await RNFS.mkdir(this.cacheDir);
      await this.loadCacheIndex();
    } catch (error) {
      console.error('Failed to initialize cache:', error);
    }
  }

  private async loadCacheIndex(): Promise<void> {
    try {
      const files = await RNFS.readDir(this.cacheDir);
      
      for (const file of files) {
        if (file.isFile()) {
          this.cacheIndex.set(file.name, {
            filePath: file.path,
            lastAccessed: file.mtime.getTime(),
            size: file.size,
          });
        }
      }
    } catch (error) {
      console.error('Failed to load cache index:', error);
    }
  }

  async getCachedFile(key: string): Promise<string | null> {
    const entry = this.cacheIndex.get(key);
    
    if (entry) {
      const exists = await RNFS.exists(entry.filePath);
      if (exists) {
        // Update last accessed time
        entry.lastAccessed = Date.now();
        return entry.filePath;
      } else {
        // File was deleted, remove from index
        this.cacheIndex.delete(key);
      }
    }
    
    return null;
  }

  async setCachedFile(key: string, sourceFilePath: string): Promise<string> {
    const destinationPath = `${this.cacheDir}/${key}`;
    
    try {
      await RNFS.copyFile(sourceFilePath, destinationPath);
      
      const stat = await RNFS.stat(destinationPath);
      const entry: CacheEntry = {
        filePath: destinationPath,
        lastAccessed: Date.now(),
        size: stat.size,
      };
      
      this.cacheIndex.set(key, entry);
      await this.enforceMaxSize();
      
      return destinationPath;
    } catch (error) {
      console.error('Failed to cache file:', error);
      throw error;
    }
  }

  private async enforceMaxSize(): Promise<void> {
    const totalSize = Array.from(this.cacheIndex.values())
      .reduce((sum, entry) => sum + entry.size, 0);
    
    if (totalSize <= this.maxCacheSize) {
      return;
    }

    // Sort entries by last accessed time (LRU)
    const sortedEntries = Array.from(this.cacheIndex.entries())
      .sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed);

    let currentSize = totalSize;
    for (const [key, entry] of sortedEntries) {
      if (currentSize <= this.maxCacheSize) {
        break;
      }

      try {
        await RNFS.unlink(entry.filePath);
        this.cacheIndex.delete(key);
        currentSize -= entry.size;
      } catch (error) {
        console.error('Failed to delete cached file:', error);
      }
    }
  }

  async clearCache(): Promise<void> {
    try {
      const files = await RNFS.readDir(this.cacheDir);
      
      for (const file of files) {
        await RNFS.unlink(file.path);
      }
      
      this.cacheIndex.clear();
    } catch (error) {
      console.error('Failed to clear cache:', error);
    }
  }
}

6. Common Pitfalls and How to Avoid Them

Platform-Specific Path Issues

Pitfall: Using iOS-specific paths on Android or vice versa.
// ❌ Wrong: Platform-specific code without checks
const imagePath = RNFS.MainBundlePath + '/assets/image.png'; // iOS only!

// ✅ Correct: Platform-aware path handling
import { Platform } from 'react-native';

function getAssetPath(assetName: string): string {
  if (Platform.OS === 'ios') {
    return `${RNFS.MainBundlePath}/${assetName}`;
  } else {
    // On Android, use assets folder or bundle resources
    return `asset://${assetName}`;
  }
}

// ✅ Better: Use cross-platform approach
function getCrossPlatformPath(relativePath: string): string {
  return `${RNFS.DocumentDirectoryPath}/${relativePath}`;
}

Permission Handling

Pitfall: Not requesting proper permissions before accessing external storage.
import { PermissionsAndroid, Platform } from 'react-native';

async function requestStoragePermission(): Promise<boolean> {
  if (Platform.OS !== 'android') {
    return true; // iOS handles permissions automatically
  }

  try {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
      {
        title: 'Storage Permission',
        message: 'This app needs access to storage to save files.',
        buttonNeutral: 'Ask Me Later',
        buttonNegative: 'Cancel',
        buttonPositive: 'OK',
      }
    );

    return granted === PermissionsAndroid.RESULTS.GRANTED;
  } catch (error) {
    console.error('Permission request failed:', error);
    return false;
  }
}

// ✅ Always check permissions before external storage operations
async function saveToExternalStorage(content: string, filename: string): Promise<void> {
  const hasPermission = await requestStoragePermission();
  
  if (!hasPermission) {
    throw new Error('Storage permission denied');
  }

  const filePath = `${RNFS.ExternalStorageDirectoryPath}/${filename}`;
  await RNFS.writeFile(filePath, content, 'utf8');
}

Error Handling and Recovery

Pitfall: Not handling file operation errors properly, leading to app crashes.
// ❌ Wrong: No error handling
async function badFileOperation() {
  const content = await RNFS.readFile('/some/path.txt', 'utf8');
  return content;
}

// ✅ Correct: Comprehensive error handling
async function robustFileOperation(filePath: string): Promise<string | null> {
  try {
    // Check if file exists first
    const exists = await RNFS.exists(filePath);
    if (!exists) {
      console.warn('File does not exist:', filePath);
      return null;
    }

    // Check if it's actually a file (not a directory)
    const stat = await RNFS.stat(filePath);
    if (!stat.isFile()) {
      console.warn('Path is not a file:', filePath);
      return null;
    }

    // Read the file
    const content = await RNFS.readFile(filePath, 'utf8');
    return content;
  } catch (error) {
    console.error('File operation failed:', error);
    
    // Handle specific error types
    if (error.code === 'ENOENT') {
      console.error('File not found:', filePath);
    } else if (error.code === 'EACCES') {
      console.error('Permission denied:', filePath);
    } else if (error.code === 'EISDIR') {
      console.error('Path is a directory, not a file:', filePath);
    }
    
    return null;
  }
}

// ✅ Retry mechanism for network operations
async function downloadWithRetry(
  url: string,
  filePath: string,
  maxRetries: number = 3
): Promise<void> {
  let lastError: Error;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await RNFS.downloadFile({
        fromUrl: url,
        toFile: filePath,
      }).promise;
      
      if (result.statusCode === 200) {
        return; // Success
      } else {
        throw new Error(`HTTP ${result.statusCode}`);
      }
    } catch (error) {
      lastError = error as Error;
      console.warn(`Download attempt ${attempt} failed:`, error);
      
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }
  
  throw new Error(`Download failed after ${maxRetries} attempts: ${lastError.message}`);
}

Memory Leaks in Large File Operations

Pitfall: Loading entire large files into memory, causing out-of-memory errors.
// ❌ Wrong: Loading entire large file into memory
async function processLargeFileBad(filePath: string): Promise<void> {
  const content = await RNFS.readFile(filePath, 'utf8'); // Could crash on large files
  // Process content...
}

// ✅ Correct: Stream processing for large files
async function processLargeFileGood(filePath: string): Promise<void> {
  const stat = await RNFS.stat(filePath);
  const fileSize = stat.size;
  const chunkSize = 64 * 1024; // 64KB chunks
  
  for (let position = 0; position < fileSize; position += chunkSize) {
    const remainingSize = fileSize - position;
    const currentChunkSize = Math.min(chunkSize, remainingSize);
    
    const chunk = await RNFS.read(filePath, currentChunkSize, position, 'utf8');
    
    // Process chunk
    await processChunk(chunk);
    
    // Allow garbage collection
    await new Promise(resolve => setImmediate(resolve));
  }
}

async function processChunk(chunk: string): Promise<void> {
  // Process the chunk without keeping references
  // This allows the previous chunk to be garbage collected
}

7. Best Practices and Patterns

File Operation Manager

Create a centralized file manager to handle all file operations consistently across your app.

class FileManager {
  private static instance: FileManager;
  private readonly baseDir: string;

  private constructor() {
    this.baseDir = RNFS.DocumentDirectoryPath;
  }

  static getInstance(): FileManager {
    if (!FileManager.instance) {
      FileManager.instance = new FileManager();
    }
    return FileManager.instance;
  }

  async ensureDirectoryExists(relativePath: string): Promise<string> {
    const fullPath = `${this.baseDir}/${relativePath}`;
    const exists = await RNFS.exists(fullPath);
    
    if (!exists) {
      await RNFS.mkdir(fullPath);
    }
    
    return fullPath;
  }

  async saveUserData(filename: string, data: any): Promise<string> {
    const userDataDir = await this.ensureDirectoryExists('user_data');
    const filePath = `${userDataDir}/${filename}`;
    
    await RNFS.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
    return filePath;
  }

  async loadUserData<T>(filename: string): Promise<T | null> {
    const filePath = `${this.baseDir}/user_data/${filename}`;
    
    try {
      const exists = await RNFS.exists(filePath);
      if (!exists) {
        return null;
      }

      const content = await RNFS.readFile(filePath, 'utf8');
      return JSON.parse(content) as T;
    } catch (error) {
      console.error('Failed to load user data:', error);
      return null;
    }
  }

  async cleanupTempFiles(olderThanHours: number = 24): Promise<void> {
    try {
      const tempDir = RNFS.TemporaryDirectoryPath;
      const files = await RNFS.readDir(tempDir);
      const cutoffTime = Date.now() - (olderThanHours * 60 * 60 * 1000);

      for (const file of files) {
        if (file.mtime.getTime() < cutoffTime) {
          await RNFS.unlink(file.path);
          console.log('Cleaned up temp file:', file.name);
        }
      }
    } catch (error) {
      console.error('Failed to cleanup temp files:', error);
    }
  }

  async getFileInfo(relativePath: string): Promise<RNFS.StatResult | null> {
    const fullPath = `${this.baseDir}/${relativePath}`;
    
    try {
      const exists = await RNFS.exists(fullPath);
      if (!exists) {
        return null;
      }

      return await RNFS.stat(fullPath);
    } catch (error) {
      console.error('Failed to get file info:', error);
      return null;
    }
  }
}

Async Queue for File Operations

type FileOperation = () => Promise<void>;

class FileOperationQueue {
  private queue: FileOperation[] = [];
  private isProcessing: boolean = false;
  private readonly maxConcurrent: number;
  private activeOperations: number = 0;

  constructor(maxConcurrent: number = 3) {
    this.maxConcurrent = maxConcurrent;
  }

  async addOperation<T>(operation: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      const wrappedOperation = async () => {
        try {
          const result = await operation();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };

      this.queue.push(wrappedOperation);
      this.processQueue();
    });
  }

  private async processQueue(): Promise<void> {
    if (this.isProcessing || this.activeOperations >= this.maxConcurrent) {
      return;
    }

    if (this.queue.length === 0) {
      return;
    }

    this.isProcessing = true;
    const operation = this.queue.shift()!;
    this.activeOperations++;

    try {
      await operation();
    } catch (error) {
      console.error('File operation failed:', error);
    } finally {
      this.activeOperations--;
      this.isProcessing = false;
      
      // Process next operation if available
      if (this.queue.length > 0) {
        setImmediate(() => this.processQueue());
      }
    }
  }

  async waitForAll(): Promise<void> {
    while (this.queue.length > 0 || this.activeOperations > 0) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }
}

// Usage example
const fileQueue = new FileOperationQueue(2); // Max 2 concurrent operations

async function batchDownloadImages(urls: string[]): Promise<string[]> {
  const downloadPromises = urls.map((url, index) => 
    fileQueue.addOperation(async () => {
      const filename = `image_${index}.jpg`;
      const filePath = `${RNFS.DocumentDirectoryPath}/images/${filename}`;
      
      await RNFS.downloadFile({
        fromUrl: url,
        toFile: filePath,
      }).promise;
      
      return filePath;
    })
  );

  return Promise.all(downloadPromises);
}

8. Testing File Operations

Unit Testing with Jest

// __tests__/FileManager.test.ts
import RNFS from 'react-native-fs';
import { FileManager } from '../src/FileManager';

// Mock react-native-fs
jest.mock('react-native-fs', () => ({
  DocumentDirectoryPath: '/mock/documents',
  exists: jest.fn(),
  mkdir: jest.fn(),
  writeFile: jest.fn(),
  readFile: jest.fn(),
  stat: jest.fn(),
  unlink: jest.fn(),
}));

describe('FileManager', () => {
  let fileManager: FileManager;
  
  beforeEach(() => {
    fileManager = FileManager.getInstance();
    jest.clearAllMocks();
  });

  describe('saveUserData', () => {
    it('should create directory and save file', async () => {
      const mockData = { user: 'test', preferences: {} };
      
      (RNFS.exists as jest.Mock).mockResolvedValue(false);
      (RNFS.mkdir as jest.Mock).mockResolvedValue(undefined);
      (RNFS.writeFile as jest.Mock).mockResolvedValue(undefined);

      const result = await fileManager.saveUserData('user.json', mockData);

      expect(RNFS.mkdir).toHaveBeenCalledWith('/mock/documents/user_data');
      expect(RNFS.writeFile).toHaveBeenCalledWith(
        '/mock/documents/user_data/user.json',
        JSON.stringify(mockData, null, 2),
        'utf8'
      );
      expect(result).toBe('/mock/documents/user_data/user.json');
    });
  });

  describe('loadUserData', () => {
    it('should return null if file does not exist', async () => {
      (RNFS.exists as jest.Mock).mockResolvedValue(false);

      const result = await fileManager.loadUserData('user.json');

      expect(result).toBeNull();
    });

    it('should load and parse JSON data', async () => {
      const mockData = { user: 'test' };
      
      (RNFS.exists as jest.Mock).mockResolvedValue(true);
      (RNFS.readFile as jest.Mock).mockResolvedValue(JSON.stringify(mockData));

      const result = await fileManager.loadUserData('user.json');

      expect(result).toEqual(mockData);
    });
  });
});

9. Conclusion

React Native FS is a powerful library for handling file system operations in mobile applications. By following the best practices outlined in this guide, you can:

  • Implement efficient file operations that don't block the UI
  • Handle large files without memory issues
  • Provide proper error handling and recovery mechanisms
  • Create robust caching strategies
  • Avoid common platform-specific pitfalls
  • Build maintainable and testable file management systems

Remember to always test your file operations thoroughly across different devices and scenarios, especially when dealing with large files or network operations. Proper error handling and user feedback are crucial for a good user experience.

Pro Tip: Consider using react-native-fs in combination with other libraries like react-native-blob-util for more advanced use cases, or react-native-document-picker for file selection functionality.

🚀 Ready to catch UI issues before your users do?

Use Viewlytics to automatically capture and analyze your app's UI across real devices. Our AI-powered platform helps you identify visual bugs, layout issues, and inconsistencies before they impact user experience.

Start UI Testing with Viewlytics
logo

2025 © Viewlytics. All rights reserved.