React Native Support
Dropup works in React Native with the native adapter.
Installation
npm install @samithahansaka/dropup
# For image picking
npx expo install expo-image-picker
# For document picking
npx expo install expo-document-picker
# For image manipulation (optional)
npx expo install expo-image-manipulator
Basic Setup
import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import { View, Text, Button, Image, FlatList } from 'react-native';
function NativeUploader() {
const { files, actions, state } = useDropup({
// Use native adapter
adapter: NativeAdapter,
upload: {
url: 'https://your-api.com/upload',
},
});
const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
quality: 0.8,
});
if (!result.canceled) {
// Convert to File-like objects
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'photo.jpg',
type: asset.mimeType || 'image/jpeg',
}));
actions.addFiles(files);
}
};
return (
<View style={styles.container}>
<Button title="Pick Images" onPress={pickImage} />
<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.preview} />
)}
<Text>{item.name}</Text>
<Text>{item.status}</Text>
{item.status === 'uploading' && (
<Text>{item.progress}%</Text>
)}
</View>
)}
/>
<Button
title="Upload All"
onPress={() => actions.upload()}
disabled={state.isUploading}
/>
</View>
);
}
const styles = {
container: {
flex: 1,
padding: 20,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
preview: {
width: 50,
height: 50,
marginRight: 10,
borderRadius: 4,
},
};
Image Picker Integration
Pick from Gallery
import * as ImagePicker from 'expo-image-picker';
const pickFromGallery = async () => {
// Request permission
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Permission required');
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: true,
quality: 0.8,
selectionLimit: 10,
});
if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
size: asset.fileSize,
}));
actions.addFiles(files);
}
};
Take Photo
const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
alert('Camera permission required');
return;
}
const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
allowsEditing: true,
aspect: [4, 3],
});
if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: asset.mimeType || 'image/jpeg',
}]);
}
};
Document Picker
import * as DocumentPicker from 'expo-document-picker';
const pickDocument = async () => {
const result = await DocumentPicker.getDocumentAsync({
type: ['application/pdf', 'application/msword'],
multiple: true,
});
if (result.type === 'success') {
actions.addFiles([{
uri: result.uri,
name: result.name,
type: result.mimeType,
size: result.size,
}]);
}
};
Image Compression (Native)
import * as ImageManipulator from 'expo-image-manipulator';
async function compressNativeImage(uri: string): Promise<string> {
const result = await ImageManipulator.manipulateAsync(
uri,
[{ resize: { width: 1200 } }],
{ compress: 0.7, format: ImageManipulator.SaveFormat.JPEG }
);
return result.uri;
}
// Use with Dropup
const { actions } = useDropup({
onFilesAdded: async (newFiles) => {
for (const file of newFiles) {
if (file.type.startsWith('image/')) {
const compressedUri = await compressNativeImage(file.uri);
actions.updateFileMeta(file.id, { compressedUri });
}
}
},
});
Full Native Example
import React from 'react';
import {
View,
Text,
TouchableOpacity,
Image,
FlatList,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { useDropup } from '@samithahansaka/dropup';
import { NativeAdapter } from '@samithahansaka/dropup/native';
import * as ImagePicker from 'expo-image-picker';
import * as DocumentPicker from 'expo-document-picker';
export default function FileUploader() {
const { files, actions, state } = useDropup({
adapter: NativeAdapter,
maxFiles: 10,
upload: {
url: 'https://your-api.com/upload',
headers: {
'Authorization': 'Bearer token',
},
},
onUploadComplete: (file) => {
console.log('Uploaded:', file.uploadedUrl);
},
onUploadError: (file, error) => {
console.error('Failed:', error.message);
},
});
const pickImages = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') return;
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsMultipleSelection: true,
});
if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.fileName || 'image.jpg',
type: asset.mimeType || 'image/jpeg',
}));
actions.addFiles(files);
}
};
const pickDocuments = async () => {
const result = await DocumentPicker.getDocumentAsync({
multiple: true,
});
if (!result.canceled) {
const files = result.assets.map(asset => ({
uri: asset.uri,
name: asset.name,
type: asset.mimeType,
size: asset.size,
}));
actions.addFiles(files);
}
};
const takePhoto = async () => {
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') return;
const result = await ImagePicker.launchCameraAsync({
quality: 0.8,
});
if (!result.canceled) {
const asset = result.assets[0];
actions.addFiles([{
uri: asset.uri,
name: `photo_${Date.now()}.jpg`,
type: 'image/jpeg',
}]);
}
};
const renderFile = ({ item }) => (
<View style={styles.fileItem}>
{item.preview && (
<Image source={{ uri: item.preview }} style={styles.thumbnail} />
)}
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.fileStatus}>
{item.status === 'uploading'
? `Uploading ${item.progress}%`
: item.status}
</Text>
</View>
<TouchableOpacity
onPress={() => actions.remove(item.id)}
style={styles.removeBtn}
>
<Text style={styles.removeText}>×</Text>
</TouchableOpacity>
</View>
);
return (
<View style={styles.container}>
{/* Action Buttons */}
<View style={styles.buttonRow}>
<TouchableOpacity style={styles.button} onPress={pickImages}>
<Text style={styles.buttonText}>Gallery</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={takePhoto}>
<Text style={styles.buttonText}>Camera</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={pickDocuments}>
<Text style={styles.buttonText}>Files</Text>
</TouchableOpacity>
</View>
{/* File List */}
<FlatList
data={files}
keyExtractor={item => item.id}
renderItem={renderFile}
style={styles.list}
ListEmptyComponent={
<Text style={styles.emptyText}>No files selected</Text>
}
/>
{/* Upload Button */}
<TouchableOpacity
style={[styles.uploadBtn, state.isUploading && styles.uploadBtnDisabled]}
onPress={() => actions.upload()}
disabled={state.isUploading || files.length === 0}
>
{state.isUploading ? (
<View style={styles.uploadingRow}>
<ActivityIndicator color="white" />
<Text style={styles.uploadBtnText}>
Uploading {state.progress}%
</Text>
</View>
) : (
<Text style={styles.uploadBtnText}>
Upload {files.length} file(s)
</Text>
)}
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
buttonRow: {
flexDirection: 'row',
gap: 12,
marginBottom: 16,
},
button: {
flex: 1,
backgroundColor: '#f0f0f0',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
fontWeight: '600',
},
list: {
flex: 1,
},
fileItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
thumbnail: {
width: 50,
height: 50,
borderRadius: 4,
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontWeight: '500',
},
fileStatus: {
color: '#666',
fontSize: 12,
marginTop: 2,
},
removeBtn: {
padding: 8,
},
removeText: {
fontSize: 24,
color: '#999',
},
emptyText: {
textAlign: 'center',
color: '#999',
marginTop: 40,
},
uploadBtn: {
backgroundColor: '#2196f3',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 16,
},
uploadBtnDisabled: {
backgroundColor: '#ccc',
},
uploadBtnText: {
color: 'white',
fontWeight: '600',
fontSize: 16,
},
uploadingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
});
Platform Differences
| Feature | Web | React Native |
|---|---|---|
| Drag & drop | Yes | No |
| File picker | Native | expo-image-picker |
| Preview URLs | Object URLs | File URIs |
| File access | File object | URI-based |
Expo vs Bare React Native
Expo (Managed)
All the examples above work with Expo managed workflow.
Bare React Native
For bare React Native, install the native modules:
# Instead of expo packages
npm install react-native-image-picker
npm install react-native-document-picker
# Link if needed (older RN versions)
npx react-native link
Then use the corresponding APIs from those packages.