Skip to content

Expo

The Expo client provides Expo-specific implementations of the Uploadista client services, enabling file uploads through Expo’s managed APIs. Supports file picking, camera capture, chunked uploads, and real-time progress tracking for React Native applications.

npm install @uploadista/expo

Peer Dependencies:

  • expo (60.0.0+)
  • expo-file-system
  • expo-image-picker
  • expo-document-picker
  • expo-crypto
  • react (18.0.0+)
  • react-native
Terminal window
npx expo install expo-file-system expo-image-picker expo-document-picker expo-crypto
import { createUploadistaClient, UploadistaProvider } from "@uploadista/expo";
// Create client
const client = createUploadistaClient({
baseUrl: "https://api.example.com",
storageId: "my-storage",
chunkSize: 1024 * 1024, // 1MB chunks
});
// Wrap your app
function App() {
return (
<UploadistaProvider client={client}>
<MainScreen />
</UploadistaProvider>
);
}
import { useUploadistaClient } from "@uploadista/expo";
import { View, Button, Text } from "react-native";
import * as ImagePicker from "expo-image-picker";
function UploadScreen() {
const { client } = useUploadistaClient();
const handleUpload = async () => {
// Pick an image
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (result.canceled) return;
const asset = result.assets[0];
// Upload the file
const upload = await client.upload({
uri: asset.uri,
name: asset.fileName ?? "photo.jpg",
type: asset.mimeType ?? "image/jpeg",
});
console.log("Upload complete:", upload.id);
};
return (
<View>
<Button title="Pick & Upload Image" onPress={handleUpload} />
</View>
);
}

The Expo client supports two authentication modes that integrate with your existing auth system.

Use direct mode when you have your own authentication (JWT, API keys, sessions, OAuth):

import { createUploadistaClient } from "@uploadista/expo";
const client = createUploadistaClient({
baseUrl: "https://api.example.com",
storageId: "my-storage",
chunkSize: 1024 * 1024,
auth: {
mode: 'direct',
getCredentials: async () => ({
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
})
}
});

Common patterns for direct mode:

// With OAuth token (e.g., Auth0, Firebase Auth)
auth: {
mode: 'direct',
getCredentials: async () => {
const token = await auth().currentUser?.getIdToken();
return {
headers: { 'Authorization': `Bearer ${token}` }
};
}
}
// With API key from secure storage
import * as SecureStore from 'expo-secure-store';
auth: {
mode: 'direct',
getCredentials: async () => ({
headers: { 'X-API-Key': await SecureStore.getItemAsync('apiKey') }
})
}

Use UploadistaCloud mode for automatic JWT token exchange with your auth server. See the Uploadista Cloud Integration Guide for the full setup including backend auth endpoint:

const client = createUploadistaClient({
storageId: "my-storage",
auth: {
mode: 'uploadista-cloud',
authServerUrl: 'https://api.example.com/auth/token',
clientId: 'your-client-id'
}
});

Create an Uploadista client with Expo-specific services:

import { createUploadistaClient } from "@uploadista/expo";
const client = createUploadistaClient({
// Required
baseUrl: "https://api.example.com",
storageId: "my-storage",
// Optional
chunkSize: 5 * 1024 * 1024, // 5MB chunks (default: 1MB)
useAsyncStorage: true, // Persist state to AsyncStorage
connectionPooling: {
maxConnections: 6,
requestTimeout: 30000,
},
});
Option Type Default Description
baseUrl string Required Uploadista server URL
storageId string Required Storage backend ID
chunkSize number 1048576 Chunk size in bytes
useAsyncStorage boolean true Persist state to AsyncStorage
connectionPooling object - HTTP connection pool config

Wrap your app with UploadistaProvider to access client and events:

import {
UploadistaProvider,
useUploadistaContext,
createUploadistaClient,
} from "@uploadista/expo";
const client = createUploadistaClient({ /* ... */ });
function App() {
return (
<UploadistaProvider client={client}>
<Navigation />
</UploadistaProvider>
);
}

Access the client and event subscription:

import { useUploadistaContext } from "@uploadista/expo";
function UploadComponent() {
const { client, subscribeToEvents } = useUploadistaContext();
// Use client for uploads
// Subscribe to events for real-time updates
return (/* ... */);
}

Simplified hook for client access:

import { useUploadistaClient } from "@uploadista/expo";
function MyComponent() {
const { client, isReady } = useUploadistaClient();
if (!isReady) {
return <Text>Loading...</Text>;
}
return (/* ... */);
}

Utility class for picking files from device:

import { ExpoFileSystemProvider } from "@uploadista/expo";
const provider = new ExpoFileSystemProvider();
// Pick image from gallery
const image = await provider.pickImage({
mediaTypes: "images",
quality: 0.8,
allowsEditing: true,
});
// Take photo with camera
const photo = await provider.takePhoto({
quality: 0.8,
allowsEditing: true,
});
// Pick any document
const document = await provider.pickDocument({
type: ["application/pdf", "image/*"],
copyToCacheDirectory: true,
});
interface FileInfo {
uri: string;
name: string;
size: number;
mimeType: string;
}
interface FilePickResult {
canceled: boolean;
file?: FileInfo;
}

Listen to upload-specific events:

import { useUploadEvents } from "@uploadista/expo";
function UploadProgress() {
useUploadEvents({
onProgress: (event) => {
console.log(`Progress: ${event.progress}%`);
console.log(`Bytes: ${event.bytesUploaded}/${event.totalBytes}`);
},
onFile: (event) => {
console.log("File uploaded:", event.uploadId);
},
onFailed: (event) => {
console.error("Upload failed:", event.error);
},
onValidationSuccess: (event) => {
console.log("Validation passed:", event.uploadId);
},
onValidationFailed: (event) => {
console.warn("Validation failed:", event.errors);
},
});
return (/* ... */);
}

Available Callbacks:

Callback Event Data Description
onProgress { uploadId, progress, bytesUploaded, totalBytes } Upload progress update
onFile { uploadId, metadata } File upload complete
onFailed { uploadId, error } Upload failed
onValidationSuccess { uploadId } File validation passed
onValidationFailed { uploadId, errors } File validation failed
onValidationWarning { uploadId, warnings } File validation warnings

Listen to flow execution events:

import { useFlowEvents } from "@uploadista/expo";
function FlowMonitor() {
useFlowEvents({
onJobStart: (event) => {
console.log("Job started:", event.jobId);
},
onFlowStart: (event) => {
console.log("Flow started:", event.flowId);
},
onNodeStart: (event) => {
console.log("Processing:", event.nodeName);
},
onNodeEnd: (event) => {
console.log("Node complete:", event.nodeName, event.result);
},
onFlowEnd: (event) => {
console.log("Flow complete:", event.outputs);
},
onFlowError: (event) => {
console.error("Flow failed:", event.error);
},
});
return (/* ... */);
}

Available Callbacks:

Callback Description
onJobStart Job execution started
onJobEnd Job execution completed
onFlowStart Flow began execution
onFlowEnd Flow completed successfully
onFlowError Flow encountered an error
onFlowPause Flow paused by user
onFlowCancel Flow cancelled
onNodeStart Node started processing
onNodeEnd Node completed
onNodePause Node waiting for input
onNodeResume Node resumed
onNodeError Node encountered error

Low-level hook for all events:

import { useUploadistaEvents, isFlowEvent, isUploadEvent } from "@uploadista/expo";
function EventLogger() {
useUploadistaEvents((event) => {
if (isUploadEvent(event)) {
console.log("Upload event:", event);
} else if (isFlowEvent(event)) {
console.log("Flow event:", event);
}
});
return (/* ... */);
}

The useFlow hook manages flow-based uploads with multiple inputs:

import { useFlow } from "@uploadista/expo";
function FlowUpload() {
const flow = useFlow({
flowId: "image-processing",
storageId: "my-storage",
onComplete: (outputs) => {
console.log("Flow outputs:", outputs);
},
onError: (error) => {
console.error("Flow error:", error);
},
});
const handleAddImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync();
if (!result.canceled) {
flow.addInput("input-1", {
uri: result.assets[0].uri,
name: result.assets[0].fileName ?? "image.jpg",
type: result.assets[0].mimeType ?? "image/jpeg",
});
}
};
return (
<View>
<Button title="Add Image" onPress={handleAddImage} />
<Button
title="Execute Flow"
onPress={flow.execute}
disabled={!flow.canExecute}
/>
{flow.isExecuting && (
<Text>Progress: {Math.round(flow.progress * 100)}%</Text>
)}
</View>
);
}

Declarative UI for flow uploads:

import { Flow } from "@uploadista/expo";
import { View, Text, Button, Image } from "react-native";
function FlowUploadUI() {
return (
<Flow flowId="image-processing" storageId="my-storage">
{({ isExecuting, progress, canExecute }) => (
<View>
<Flow.Inputs>
{({ inputs }) => (
<View>
{inputs.map((input) => (
<Flow.Input key={input.id} inputId={input.id}>
<Flow.Input.FilePicker>
{({ pickFile }) => (
<Button title="Select File" onPress={pickFile} />
)}
</Flow.Input.FilePicker>
<Flow.Input.Preview>
{({ file, preview }) =>
preview ? (
<Image source={{ uri: preview }} style={{ width: 100, height: 100 }} />
) : null
}
</Flow.Input.Preview>
</Flow.Input>
))}
</View>
)}
</Flow.Inputs>
<Flow.Progress>
{({ progress }) => (
<Text>Progress: {Math.round(progress * 100)}%</Text>
)}
</Flow.Progress>
<Flow.Status>
{({ status }) => <Text>Status: {status}</Text>}
</Flow.Status>
<Flow.Error>
{({ error }) =>
error ? <Text style={{ color: "red" }}>{error.message}</Text> : null
}
</Flow.Error>
<Flow.Submit>
{({ submit, disabled }) => (
<Button title="Process" onPress={submit} disabled={disabled} />
)}
</Flow.Submit>
<Flow.Cancel>
{({ cancel, disabled }) => (
<Button title="Cancel" onPress={cancel} disabled={disabled} />
)}
</Flow.Cancel>
</View>
)}
</Flow>
);
}

FlowManagerProvider is already included inside UploadistaProvider, so you don’t need to add it manually for basic usage. It’s only needed if you want to access the flow manager context directly for advanced scenarios like coordinating multiple concurrent flow instances:

import { useFlowManagerContext } from "@uploadista/expo";
function AdvancedFlowList() {
// Access the flow manager for advanced multi-flow coordination
const { getManager, releaseManager } = useFlowManagerContext();
// getManager(flowId) - Get or create a flow manager instance
// releaseManager(flowId) - Release a flow manager when done
}

Note: For most use cases, simply use the Flow compound component or useFlow hook directly within UploadistaProvider.

import React, { useState } from "react";
import { View, Text, Button, Image, StyleSheet, FlatList } from "react-native";
import * as ImagePicker from "expo-image-picker";
import {
createUploadistaClient,
UploadistaProvider,
useUploadistaClient,
useUploadEvents,
useFlowEvents,
} from "@uploadista/expo";
// Create client
const client = createUploadistaClient({
baseUrl: "https://api.example.com",
storageId: "s3-production",
chunkSize: 5 * 1024 * 1024,
});
// Upload screen component
function UploadScreen() {
const { client } = useUploadistaClient();
const [uploads, setUploads] = useState<
{ id: string; name: string; progress: number; status: string }[]
>([]);
// Listen for upload events
useUploadEvents({
onProgress: (event) => {
setUploads((prev) =>
prev.map((u) =>
u.id === event.uploadId ? { ...u, progress: event.progress } : u
)
);
},
onFile: (event) => {
setUploads((prev) =>
prev.map((u) =>
u.id === event.uploadId ? { ...u, status: "complete" } : u
)
);
},
onFailed: (event) => {
setUploads((prev) =>
prev.map((u) =>
u.id === event.uploadId ? { ...u, status: "failed" } : u
)
);
},
});
// Listen for flow events
useFlowEvents({
onFlowEnd: (event) => {
console.log("Processing complete:", event.outputs);
},
});
const pickAndUpload = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
quality: 0.8,
});
if (result.canceled) return;
const asset = result.assets[0];
const uploadId = `upload-${Date.now()}`;
// Add to list
setUploads((prev) => [
...prev,
{
id: uploadId,
name: asset.fileName ?? "image.jpg",
progress: 0,
status: "uploading",
},
]);
try {
// Start upload
await client.upload({
uri: asset.uri,
name: asset.fileName ?? "image.jpg",
type: asset.mimeType ?? "image/jpeg",
});
} catch (error) {
console.error("Upload error:", error);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Uploadista Expo Demo</Text>
<Button title="Pick Image" onPress={pickAndUpload} />
<FlatList
data={uploads}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.uploadItem}>
<Text>{item.name}</Text>
<Text>
{item.status === "uploading"
? `${Math.round(item.progress * 100)}%`
: item.status}
</Text>
</View>
)}
/>
</View>
);
}
// App with provider
export default function App() {
return (
<UploadistaProvider client={client}>
<UploadScreen />
</UploadistaProvider>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, paddingTop: 60 },
title: { fontSize: 24, fontWeight: "bold", marginBottom: 20 },
uploadItem: {
flexDirection: "row",
justifyContent: "space-between",
padding: 10,
borderBottomWidth: 1,
borderBottomColor: "#eee",
},
});

Full TypeScript support with comprehensive types:

import type {
UploadistaClientOptions,
UseFlowEventsOptions,
UseUploadEventsOptions,
FlowProps,
FlowUploadState,
FlowUploadStatus,
UseFlowReturn,
FileInfo,
FilePickResult,
} from "@uploadista/expo";