# FileUploader (React Native)

> Pick, validate and upload files with progress, retry and remove. You bring the picker; Kit takes no dependency.

GramproKit for React Native · Forms · Beta · Source: https://kit.gramproindia.com/native/docs/file-uploader

> **Beta:** React Native support is in beta. APIs may change or break between prereleases (npm tag `native`).

_Interactive demo:_ [open the live example](https://kit.gramproindia.com/native/docs/file-uploader). The example's source:

```tsx
import { useState } from "react";
import { Text } from "react-native";
import {
  FileUploader,
  type NativeFile,
  type Transport,
} from "@/component-lib/file-uploader";
import { useKitTheme } from "@/component-lib/shared/native";
import { Section } from "./layout"; // Section and Row: page layout for the demo

// A real app passes a picker adapter, e.g. expo-document-picker:
//   pick={async () => (await getDocumentAsync({ multiple: true })).assets?.map(
//     (a) => ({ uri: a.uri, name: a.name, size: a.size ?? 0, type: a.mimeType ?? "" }))}
// The demo fakes one so it runs in Expo Go with no picker installed.
const SAMPLES: NativeFile[] = [
  { uri: "https://picsum.photos/id/1015/200", name: "backwaters.jpg", size: 245_760, type: "image/jpeg" },
  { uri: "file:///demo/statement-oct.pdf", name: "statement-oct.pdf", size: 1_310_720, type: "application/pdf" },
  { uri: "file:///demo/ledger.xlsx", name: "ledger.xlsx", size: 88_064, type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
  { uri: "file:///demo/huge-video.mp4", name: "huge-video.mp4", size: 52_428_800, type: "video/mp4" },
  { uri: "file:///demo/notes.txt", name: "notes.txt", size: 2_048, type: "text/plain" },
];

let picks = 0;
/** Returns two sample files per call, cycling through the list. */
async function fakePick(): Promise<NativeFile[]> {
  await new Promise((resolve) => setTimeout(resolve, 250));
  const start = (picks++ * 2) % SAMPLES.length;
  return [SAMPLES[start], SAMPLES[(start + 1) % SAMPLES.length]];
}

/** Pretends to send the file, reporting progress; PDFs fail once, to show retry. */
const failedOnce = new Set<string>();
const fakeTransport: Transport = ({ file, signal, onProgress }) =>
  new Promise((resolve, reject) => {
    let sent = 0;
    const timer = setInterval(() => {
      sent = Math.min(file.size, sent + file.size / 20);
      onProgress(sent);
      if (file.name.endsWith(".pdf") && sent > file.size / 2 && !failedOnce.has(file.name)) {
        failedOnce.add(file.name);
        clearInterval(timer);
        reject(Object.assign(new Error("Server said 400: try again"), { status: 400 }));
        return;
      }
      if (sent >= file.size) {
        clearInterval(timer);
        resolve({ id: `srv-${file.name}` });
      }
    }, 120);
    signal.addEventListener("abort", () => {
      clearInterval(timer);
      reject(Object.assign(new Error("Upload aborted"), { name: "AbortError" }));
    });
  });

export function FileUploaderDemo() {
  const { tokens } = useKitTheme();
  const [files, setFiles] = useState<NativeFile[]>([]);
  return (
    <>
      <Section title="FileUploader · multiple, with upload">
        <FileUploader
          label="Attachments"
          description="Tap to add two sample files. PDFs fail once so you can retry."
          multiple
          maxFiles={6}
          maxSize={20 * 1024 * 1024}
          pick={fakePick}
          transport={fakeTransport}
          retries={0}
          onChange={setFiles}
        />
        <Text style={{ color: tokens.muted, fontSize: tokens.fontSize - 2 }}>
          Selected: {files.map((file) => file.name).join(", ") || "none"}
        </Text>
      </Section>

      <Section title="FileUploader · single image, auto upload, existing file">
        <FileUploader
          label="Profile photo"
          accept="image/*"
          autoUpload
          pick={async () => [SAMPLES[0]]}
          transport={fakeTransport}
          existingFiles={[{ id: "old", name: "old-photo.png", size: 51_200, type: "image/png" }]}
          onRemoveExisting={() => undefined}
          size="sm"
        />
      </Section>

      <Section title="FileUploader · states">
        <FileUploader label="Select only (no transport)" pick={fakePick} multiple required />
        <FileUploader label="Disabled" pick={fakePick} disabled />
        <FileUploader label="With error" pick={fakePick} error="Add at least one document." />
      </Section>
    </>
  );
}
```

## Install

```bash
npx @grampro/kit@native add FileUploader
```

## Usage

```tsx
import { FileUploader } from "@/component-lib/file-uploader";

import * as DocumentPicker from "expo-document-picker";

<FileUploader
  endpoint="/api/upload"
  maxSize={5_000_000}
  pick={async () => {
    const result = await DocumentPicker.getDocumentAsync({ multiple: true });
    return result.canceled
      ? []
      : result.assets.map((a) => ({ uri: a.uri, name: a.name, size: a.size ?? 0, type: a.mimeType ?? "" }));
  }}
/>
```

## Props

### FileUploaderProps

| Prop | Type | Description |
|---|---|---|
| `pick` | `PickFiles` | Opens a document or image picker and resolves with what was chosen. Kit takes no picker dependency: pass expo-document-picker, expo-image-picker, or anything that returns `{ uri, name, size, type }`. |
| `multiple` | `boolean` | Several files. When false, a new file replaces the current one. Default false. |
| `accept` | `string` | Same syntax as `<input accept>`: `.pdf`, `image/*`, `application/json`. |
| `maxFiles` | `number` |   |
| `maxSize` | `number` | Bytes per file. |
| `minSize` | `number` | Bytes per file. |
| `validate` | `(file: NativeFile) => string \| null \| undefined` | Return a message to reject a file. |
| `allowDuplicates` | `boolean` |   |
| `endpoint` | `HttpTransportOptions["endpoint"]` | Where files are POSTed, one multipart request per file with the same fields as the web's chunked upload (as a single chunk). Without `endpoint` or `transport`, files are only selected. |
| `method` | `HttpTransportOptions["method"]` |   |
| `headers` | `HttpTransportOptions["headers"]` |   |
| `withCredentials` | `boolean` |   |
| `params` | `HttpTransportOptions["params"]` |   |
| `fieldNames` | `HttpTransportOptions["fieldNames"]` |   |
| `parseResponse` | `HttpTransportOptions["parseResponse"]` |   |
| `transport` | `Transport` | Replaces the built-in HTTP transport, e.g. for presigned URLs or an SDK. `request.file` and `request.chunk` are the whole file, with its `uri` (see `fromUploadable`). |
| `concurrency` | `number` | Files uploading at once. Default 3. |
| `retries` | `number` | Extra attempts per file. Default 3. |
| `retryDelay` | `number` | First retry delay in ms, doubled each time. Default 1000. |
| `autoUpload` | `boolean` | Upload as soon as files are added. Default false. |
| `preview` | `boolean` | Thumbnails for images. Default true. |
| `existingFiles` | `readonly ExistingFile[]` |   |
| `onRemoveExisting` | `(file: ExistingFile) => void` |   |
| `removable` | `boolean` | Show remove buttons. Default true. |
| `label` | `ReactNode` |   |
| `description` | `ReactNode` |   |
| `error` | `ReactNode` | Message below the picker; also marks it invalid. |
| `required` | `boolean` |   |
| `disabled` | `boolean` |   |
| `size` | `UploaderSize` | Default `md`. |
| `locale` | `string` | For sizes and percentages. |
| `style` | `StyleProp<ViewStyle>` |   |
| `styles` | `Partial<Record<UploaderSlot, StyleProp<ViewStyle>>>` |   |
| `localeText` | `Partial<UploaderLocaleText>` |   |
| `onChange` | `(files: NativeFile[]) => void` |   |
| `onRejected` | `(rejections: FileRejection[]) => void` |   |
| `onFileSuccess` | `(item: UploadItem) => void` |   |
| `onFileError` | `(item: UploadItem, error: unknown) => void` |   |
| `onUploadComplete` | `(items: UploadItem[]) => void` |   |

## Native and web

- `pick()` returns `{ uri, name, size, type }` files from any picker (expo-document-picker, expo-image-picker, …).
- Each file uploads in one request; there is no chunking without a filesystem library.
- Web version: [FileUploader on the web](https://kit.gramproindia.com/fileuploader).
