KhueApps
Home/React Native/React Native Expo: Recursively List All Files and Folders

React Native Expo: Recursively List All Files and Folders

Last updated: October 08, 2025

Overview

Need to walk a directory tree in a React Native Expo app and list every subfolder and file? This guide shows how to do it with expo-file-system, plus Android external storage via StorageAccessFramework (SAF).

Supported targets:

  • iOS/Android sandbox: FileSystem.documentDirectory, FileSystem.cacheDirectory
  • Android external storage (user-picked folder): StorageAccessFramework

Quick summary: read entries with readDirectoryAsync, stat each entry with getInfoAsync, recurse into directories, and collect results.

Quickstart

  1. Install dependency
  • expo install expo-file-system
  1. Choose a root directory
  • Sandboxed (works on iOS and Android): FileSystem.documentDirectory
  • Android external folder: ask the user via StorageAccessFramework
  1. Recursively walk the tree
  • List names, build child URIs, stat each, recurse into directories
  1. Render in your component or process as needed

Minimal working example (sandboxed app directory)

The example below creates a tiny folder tree in the app’s document directory, then recursively lists everything.

import React, { useEffect, useState } from 'react';
import { Button, ScrollView, Text, View } from 'react-native';
import * as FileSystem from 'expo-file-system';

// Join a child name onto a base URI safely
const joinUri = (base: string, name: string) => (base.endsWith('/') ? base + name : base + '/' + name);

// Recursively list a directory tree
// Returns an array of { type: 'file'|'dir', uri: string, size?: number|null }
async function listRecursive(uri: string): Promise<Array<{type: 'file'|'dir'; uri: string; size?: number|null}>> {
  const names = await FileSystem.readDirectoryAsync(uri);

  // Stat all entries in parallel (moderate batch size is safer for very large trees)
  const entries = await Promise.all(
    names.map(async (name) => {
      const childUri = joinUri(uri, name);
      const info = await FileSystem.getInfoAsync(childUri);
      return { name, childUri, info } as const;
    })
  );

  const out: Array<{type: 'file'|'dir'; uri: string; size?: number|null}> = [];

  for (const { childUri, info } of entries) {
    if (!info.exists) continue;
    if (info.isDirectory) {
      out.push({ type: 'dir', uri: childUri });
      const sub = await listRecursive(childUri);
      out.push(...sub);
    } else {
      out.push({ type: 'file', uri: childUri, size: info.size ?? null });
    }
  }

  return out;
}

export default function App() {
  const [items, setItems] = useState<Array<{type: 'file'|'dir'; uri: string; size?: number|null}>>([]);
  const root = FileSystem.documentDirectory!; // ends with '/'

  // Optional: create a sample tree once
  useEffect(() => {
    (async () => {
      const a = joinUri(root, 'demo');
      const b = joinUri(a, 'nested');
      await FileSystem.makeDirectoryAsync(a, { intermediates: true }).catch(() => {});
      await FileSystem.makeDirectoryAsync(b, { intermediates: true }).catch(() => {});
      await FileSystem.writeAsStringAsync(joinUri(a, 'hello.txt'), 'Hello');
      await FileSystem.writeAsStringAsync(joinUri(b, 'world.txt'), 'World');
    })();
  }, []);

  const scan = async () => {
    const all = await listRecursive(root);
    setItems(all);
  };

  return (
    <View style={{ paddingTop: 60, paddingHorizontal: 16, flex: 1 }}>
      <Button title="Scan documentDirectory" onPress={scan} />
      <ScrollView style={{ marginTop: 12 }}>
        {items.map((it, i) => (
          <Text key={i} style={{ fontFamily: 'Courier', marginBottom: 4 }}>
            {it.type === 'dir' ? 'DIR ' : 'FILE'}  {it.uri}{it.type === 'file' && it.size != null ? ` (${it.size} B)` : ''}
          </Text>
        ))}
      </ScrollView>
    </View>
  );
}

Notes:

  • The example walks FileSystem.documentDirectory, which is always accessible.
  • getInfoAsync returns exists, isDirectory, size (if available), and uri.

Android: Recursively list a user-picked external folder (SAF)

On Android, to access outside the app sandbox (e.g., Downloads), use StorageAccessFramework to let the user pick a directory.

import * as FileSystem from 'expo-file-system';
const { StorageAccessFramework } = FileSystem;

const isDir = async (uri: string) => {
  try {
    const info = await FileSystem.getInfoAsync(uri);
    return info.exists && info.isDirectory === true;
  } catch {
    return false;
  }
};

async function listRecursiveSAF(dirUri: string): Promise<string[]> {
  const children = await StorageAccessFramework.readDirectoryAsync(dirUri);
  // children are content:// URIs
  const out: string[] = [];

  // Batch stat checks
  const infoList = await Promise.all(children.map((u) => FileSystem.getInfoAsync(u).catch(() => ({ exists: false }))));

  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    const info: any = infoList[i];
    if (!info || !info.exists) continue;
    if (info.isDirectory) {
      out.push(child);
      const sub = await listRecursiveSAF(child);
      out.push(...sub);
    } else {
      out.push(child);
    }
  }

  return out;
}

export async function pickAndList() {
  const perm = await StorageAccessFramework.requestDirectoryPermissionsAsync();
  if (!perm.granted) return [];
  return listRecursiveSAF(perm.directoryUri);
}

Tip: SAF URIs are not normal file:// paths. Keep them as returned and pass them back to expo-file-system APIs.

Step-by-step

  1. Install
  • expo install expo-file-system
  1. Decide scope
  • App sandbox (documentDirectory/cacheDirectory) or external storage (Android SAF)
  1. Implement recursion
  • readDirectoryAsync(root) → names/URIs
  • For each entry: getInfoAsync(child)
  • If isDirectory, recurse; else collect file
  1. Render or process
  • Map over results or filter by extension/size

Common pitfalls

  • Missing trailing slash: documentDirectory usually ends with '/', but other URIs may not. Normalize when joining.
  • iOS external storage: Not supported. You can only access the app sandbox.
  • Android external storage: Use StorageAccessFramework; legacy READ/WRITE permissions won’t help in Managed Expo.
  • Large trees: Unbounded parallel getInfoAsync calls can be slow or memory heavy. Batch them.
  • Asset/bundle files: App bundle assets are not enumerable via expo-file-system; use expo-asset or embed metadata.
  • File info variability: size or modificationTime may be null/undefined on some platforms/URIs.
  • Nonexistent roots: Always check getInfoAsync(root).exists before scanning.

Performance notes

  • Batch and limit concurrency: Process entries in chunks (e.g., size 16–64) to avoid overwhelming the bridge.
  • Short-circuit with filters: Pass a predicate (e.g., allowed extensions) and skip recursion into folders you don’t need.
  • Avoid MD5: Don’t request md5 in getInfoAsync unless you actually need it.
  • Shallow scans first: Collect directory structure first, then drill down selectively.
  • Cache results: Persist a snapshot (e.g., JSON in documentDirectory) and rescan incrementally.

Example: batched stat helper

async function statBatch(uris: string[], batchSize = 32) {
  const out: any[] = [];
  for (let i = 0; i < uris.length; i += batchSize) {
    const slice = uris.slice(i, i + batchSize);
    const infos = await Promise.all(slice.map((u) => FileSystem.getInfoAsync(u).catch(() => ({ exists: false }))));
    out.push(...infos);
  }
  return out;
}

Where can I list files?

LocationWorks onHow
documentDirectoryiOS, AndroidFileSystem.readDirectoryAsync + getInfoAsync
cacheDirectoryiOS, AndroidSame as above
External storage (Downloads, etc.)AndroidStorageAccessFramework after user picks a folder

FAQ

  • How do I list the app bundle files?

    • You generally can’t enumerate bundle contents. Bundle assets are compiled; track them via code or use expo-asset for known resources.
  • Do I need permissions for documentDirectory?

    • No. It’s inside your app sandbox.
  • Can I scan the entire device on Android?

    • Not automatically. You must ask the user to pick a directory via SAF, then you can traverse within that tree.
  • Why do I get undefined size?

    • Some URIs don’t expose size reliably. Treat it as optional.
  • How do I stop a long scan?

    • Add an abort flag or pass a maximum depth and check it before recursing.

Summary

  • Use expo-file-system to traverse sandboxed directories on iOS and Android.
  • On Android, use StorageAccessFramework for user-picked external folders.
  • Recurse with readDirectoryAsync + getInfoAsync, and apply batching, filtering, and caching to keep scans fast and responsive.

Series: React Native Intermediate tutorials

React Native