UPLOADING FILES FROM REACT NATIVE TO AN S3 PRESIGNED URL.
Introduction
Been off here for a while because I have mostly been building ..scratch that.. it's been more of fighting with really stubborn bugs π€¦π½ββοΈ. I am not done yet but I have decided to take some time out to share my learnings and bug fixes through a series of articles. I honestly didn't expect uploading documents to an AWS pre-signed URL to be as stressful as it was for me. Luckily, I got that solved and here is what I did (errmm yeah.. I checked Stack Overflow and it didn't work for me...for those of you already thinking that π).
This article, like the rest of my previous articles, is to serve as a guide to anyone who is just like me. Having tried the common methods, you may have found a solution. Now, I must point out that I am a noob when it comes to backend code/architecture, as I was the frontend guy on this.
I will be working with Expo and TypeScript, but you can also use this with React Native and JavaScript too.
1. Install the Needed Dependencies
Firstly, the following packages need to be installed:
expo install expo-document-picker
npm i expo-file-system
npm i buffer
2. Picking Our File
Next, we will create our file picker function, which when called will open up a document picker and try to get the image data. If successful, it will save it as a state value.
import * as DocumentPicker from 'expo-document-picker'
interface FileInterface {
type: 'success'
name: string
size: number
uri: string
lastModified?: number | undefined
file?: File | undefined
output?: FileList | null | undefined
}
//--> called when the file is picked
const pickFile = async () => {
try {
const file = await DocumentPicker.getDocumentAsync({
type: '*/*',
copyToCacheDirectory: true,
})
setFile(file)
} catch (error) {
// toast error
}
}
3. Converting to a Blob
The next step is generating a blob file using the Fetch API.
const getBlob = async (fileUri: string) => {
try {
const resp = await fetch(fileUri)
const imageBody = await resp.blob()
return imageBody
} catch (err) {
// toast error
}
}
4. Getting Your Signed URL
const signedUrl = await axiosClient.post(`${ENV.GENERATED_SIGNED_URL}`, payload)
if (signedUrl == null) {
// Toast error
return
} else {
const { url, uploadID, fileName } = signedUrl.data.data
}
5. Converting Blob URI Data to Base64
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64,
})
6. Converting Base64 Data to a Buffer
const buffer = Buffer.from(base64, 'base64')
7. Uploading to S3
const uploadFile = await axios({
method: 'PUT',
url: url,
data: buffer,
headers: { 'Content-Type': fileType ?? 'image/jpeg' },
})
8. Putting It All Together
import { useState, useEffect } from 'react';
import axios from 'axios';
import * as DocumentPicker from 'expo-document-picker';
import axiosClient from 'utils/axiosClient';
import * as FileSystem from 'expo-file-system';
import { Buffer } from 'buffer';
interface FileInterface {
type: 'success';
name: string;
size: number;
uri: string;
lastModified?: number;
file?: File;
output?: FileList | null;
}
interface DummySignedURLPayloadInterface {
[key: string]: string;
}
const signedURLpayload: DummySignedURLPayloadInterface = {
name: 'mazi_juls',
twitterhandle: '@ajulibe',
id: '007',
};
export const useUploadToS3 = () => {
const [file, setFile] = useState<FileInterface>();
const [res, setRes] = useState('π');
useEffect(() => {
if (typeof file === 'undefined') return;
uploadFileToS3(signedURLpayload, file);
}, [file]);
async function getBlob(fileUri: string) {
const resp = await fetch(fileUri);
const imageBody = await resp.blob();
return imageBody;
}
const pickFile = async () => {
const file = await DocumentPicker.getDocumentAsync({
type: '*/*',
copyToCacheDirectory: true,
});
if (file.type !== 'success') return;
setFile(file);
};
const uploadFileToS3 = async (
signedURLpayload: DummySignedURLPayloadInterface,
file: FileInterface,
) => {
try {
if (file.type !== 'success') return;
const { uri } = file;
const fileBody = await getBlob(uri);
const fileType = fileBody['type'];
if (!fileBody) return;
const signedUrl = await axiosClient.post('Upload/Generates3URL', signedURLpayload);
if (!signedUrl) throw new Error('π©π©');
const { url } = signedUrl.data.data;
const base64 = await FileSystem.readAsStringAsync(uri, {
encoding: FileSystem.EncodingType.Base64,
});
const buffer = Buffer.from(base64, 'base64');
const uploadFile = await axios({
method: 'PUT',
url: url,
data: buffer,
headers: { 'Content-Type': fileType ?? 'image/jpeg' },
});
if (uploadFile.status === 200) {
console.log('π₯³π₯³');
setRes('π₯³π₯³');
} else {
console.log('π©π©');
throw new Error('π©π©');
}
} catch (error) {
setRes('π©π©');
console.log('π©π©');
}
};
return { res, pickFile };
};
// Hook usage example
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useUploadToS3 } from './yourHooksFolder';
const MazisFuncCmp: React.FC = () => {
const { res, pickFile } = useUploadToS3();
return (
<View>
<Text>Hi There...</Text>
<Text>{res}</Text>
<TouchableOpacity type="button" onPress={pickFile}>
<Text>Click me to test π¨π½βπ«</Text>
</TouchableOpacity>
</View>
);
};
export default MazisFuncCmp;
Summary
After a file has been picked successfully, its value is stored in state. This triggers the useEffect
hook to run. At this point, the file is not undefined, so the upload function is called. The uploadFileToS3
function wraps all the logic above into a clean async flow. βπ½