Skip to content
React NativeBeta

FileUploader

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

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

On this page
Web preview — real device may differ
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

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

PropTypeDescription
pickPickFilesOpens 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 }.
multiplebooleanSeveral files. When false, a new file replaces the current one. Default false.
acceptstringSame syntax as <input accept>: .pdf, image/*, application/json.
maxFilesnumber
maxSizenumberBytes per file.
minSizenumberBytes per file.
validate(file: NativeFile) => string | null | undefinedReturn a message to reject a file.
allowDuplicatesboolean
endpointHttpTransportOptions["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.
methodHttpTransportOptions["method"]
headersHttpTransportOptions["headers"]
withCredentialsboolean
paramsHttpTransportOptions["params"]
fieldNamesHttpTransportOptions["fieldNames"]
parseResponseHttpTransportOptions["parseResponse"]
transportTransportReplaces 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).
concurrencynumberFiles uploading at once. Default 3.
retriesnumberExtra attempts per file. Default 3.
retryDelaynumberFirst retry delay in ms, doubled each time. Default 1000.
autoUploadbooleanUpload as soon as files are added. Default false.
previewbooleanThumbnails for images. Default true.
existingFilesreadonly ExistingFile[]
onRemoveExisting(file: ExistingFile) => void
removablebooleanShow remove buttons. Default true.
labelReactNode
descriptionReactNode
errorReactNodeMessage below the picker; also marks it invalid.
requiredboolean
disabledboolean
sizeUploaderSizeDefault md.
localestringFor sizes and percentages.
styleStyleProp<ViewStyle>
stylesPartial<Record<UploaderSlot, StyleProp<ViewStyle>>>
localeTextPartial<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.