---
title: React Native
description: The same useFiles hook in React Native / Expo — absolute endpoint, picker-descriptor uploads with real progress, buffered downloads.
---

`files-sdk/react` has no DOM dependency, so `useFiles` works in React Native and Expo as-is — same verbs, same progress and error state, talking to the same [gateway](/docs/ui/server/gateway) your web app uses. Two platform differences matter: the endpoint must be absolute, and uploads take the picker's `{ uri, name, type }` descriptor directly.

```tsx lineNumbers
const files = useFiles({
  endpoint: "https://app.example.com/api/files", // React Native fetch needs an absolute URL
  headers: () => ({ authorization: `Bearer ${token}` }), // your auth token
});
```

## Uploading from a picker

Pass the picker asset straight to `upload` as a `NativeFileRef` — the `{ uri, name, type }` shape `expo-document-picker` / `expo-image-picker` already return:

```tsx lineNumbers
import * as DocumentPicker from "expo-document-picker";

const pick = async () => {
  const result = await DocumentPicker.getDocumentAsync();
  if (result.canceled) {
    return;
  }
  const { uri, name, mimeType, size } = result.assets[0];
  const { key } = await files.upload(
    { uri, name, type: mimeType, size },
    { onProgress: (p) => setProgress(p.fraction) } // 0 → 1
  );
};
```

The client picks the right wire form per target: a presigned **POST** target appends the descriptor to React Native's `FormData`, which streams it from disk without buffering; every other path (presigned PUT, the through-the-gateway upload) resolves the `uri` to a Blob first. Explicit keys work the same way — `files.upload("avatars/me.png", { uri, type: mimeType })`.

Progress is real: React Native ships `XMLHttpRequest` with upload progress events, so the default transport reports byte-level progress exactly as in the browser. Blob bodies (`fetch(uri).then((r) => r.blob())`) still work, and byte bodies do too — `files.upload(key, bytes)` detects that React Native's `Blob` cannot wrap an `ArrayBuffer` and sends the bytes raw instead.

## Downloading

React Native's `fetch` has no response streaming, so `download()` transparently buffers the body — `arrayBuffer()` and `text()` behave exactly as on the web, and `blob()` hands back the response's native Blob. One ordering caveat: React Native's `Blob` cannot wrap raw bytes, so call `blob()` _before_ `arrayBuffer()`/`text()` if you need one — once a byte accessor has drained the response, `blob()` throws:

```tsx lineNumbers
const file = await files.download("report.pdf");
const bytes = await file.arrayBuffer();
```

For displaying or saving media, skip the byte round-trip and use a signed URL:

```tsx lineNumbers
import * as FileSystem from "expo-file-system";

<Image source={{ uri: avatarUrl }} />; // from await files.url("avatar.png")

await FileSystem.downloadAsync(
  await files.url("report.pdf"),
  FileSystem.documentDirectory + "report.pdf"
);
```

## Caveats

- `StoredFile.stream()` needs a `ReadableStream` polyfill (Hermes has none) — prefer `blob()` / `arrayBuffer()`.
- `StoredFile.text()` relies on `TextDecoder`, present on recent Hermes; polyfill it on older runtimes.
- Presigned **POST** upload targets (S3-family adapters with a `maxUploadSize` gateway cap) require a `NativeFileRef` body — React Native's `FormData` cannot carry a Blob part, but it streams a descriptor natively. Presigned **PUT** targets and the through-the-gateway path accept any body form.
- The [UI components](/docs/ui/components) are React DOM + Tailwind, so they are web-only; the hook state (`uploads`, `progress`, `error`) maps directly onto your native components instead.
