How to upload images and show progress bar in a React Native app
Uploading files, especially images, is one of the most common and essential use cases we might have to implement as developers when working on front-end applications, whether on a web or native environment.
We’ll go over how we can upload images on a web application in React Native using the Expo toolkit. This activity can be divided into the following tasks:
Picking an image: This task involves the user picking an image from their local system for upload. To implement this component, we’ll use the pre-built
expo-image-pickerimage picker library provided by Expo.Uploading the image: This task involves uploading the image to a backend server. We’ll use
XMLHttpRequestAPI, which is supported by all modern browsers, to send an image file to a web server through HTTP requests.Showing progress bar for image upload: This task involves adding a listener on the
XMLHttpRequestrequest to track what percentage of the total bytes of the image has been uploaded. We’ll use thereact-native-progresslibrary to create and handle the progress bar.
Installing dependencies
First of all, if we don't have it already, we need to install the expo-image-picker and react-native-progress dependencies on our system, as follows:
# Installing expo-image-pickernpx expo install expo-image-picker# Installing react-native-progressnpm i react-native-progress
Note: We do not have to install anything in the code executable provided later in this task, as all such dependencies have already been installed for you.
Importing dependencies
Once we’ve installed the expo-image-picker and react-native-progress dependencies, we can now import these and other secondary dependencies into our React Native application as follows:
import React, { useState, useEffect } from 'react';import { Button, Image, View, Text, ActivityIndicator } from 'react-native';import imgPlaceholder from '/assets/imgPlaceholder.png';import cryptoRandomString from 'crypto-random-string';import * as ImagePicker from 'expo-image-picker';import ProgressBar from 'react-native-progress/Bar';
Note the import statements for expo-image-picker and react-native-progress on lines 5–6.
Picking an image
The pickImage method defines the logic of how to handle the image selection process, which is automated by the ImagePicker method on line 4 of the expo-image-picker library.
const pickImage = async () => {// Calling image picker to let user pick imagetry {let result = await ImagePicker.launchImageLibraryAsync({mediaTypes: ImagePicker.MediaTypeOptions.All,base64: true,aspect: [4, 3],quality: 1,});// Selecting the picked imageif (!result.canceled) {setProgress(0);setLoading(true);setImage(result.assets[0]);}} catch (_) {console.log('File could not be selected');}};
We can observe the configuration settings between lines 5–8 that ImagePicker will follow when allowing the user to pick an image. Once the user selects and picks an image, we’ll save it in a state variable, as shown in line 15. To learn more on how to use ImagePicker, follow this documentation.
Uploading an image
Here’s the code for the uploadImage method that defines the implementation for uploading an image by passing it as an HTTP request.
const uploadImage = async () => {try {// Defining image URIconst imageUri = image.uri;// Creating new file nameconst currentDate = new Date(Date.now()).toISOString();const newFileExtension = imageUri.substring(imageUri.search("data:image/")+11, imageUri.search(";base64"));const newFileName = `image-${currentDate}.${newFileExtension}`;// Setting up request body and defining boundaryconst boundary = `------------------------${cryptoRandomString({ length: 16 })}`;let body = `--${boundary}\r\nContent-Disposition: form-data; name="image"; filename="${newFileName}"\r\n\r\n`;body += `${imageUri}\r\n`;body += `--${boundary}--\r\n`;// Setting up image upload requestconst request = new XMLHttpRequest();// Adding listeners to monitor upload progressrequest.upload.addEventListener('progress', trackProgress);request.addEventListener('load', () => {setProgress(100);});// Making HTTP requestrequest.open('POST', '{{EDUCATIVE_LIVE_VM_URL}}:3000/upload');request.setRequestHeader('Content-Type', `multipart/form-data; boundary=${boundary}`);request.send(body);} catch (error) {console.error('Error uploading file:', error);}};
Although, we also could have used the Fetch API to make an HTTP request with the image data, we cannot add a listener to the request, unlike the XMLHttpRequest API. When we add event listeners to an XMLHttpRequest request, we can track how bytes have been uploaded to the server in an event. The number of these upload events depends on the image size, as large files cannot be passed in a single event. We’ve added these event listeners between lines 21–24.
The code between lines 27–29 represents how we can implement a simple multipart XMLHttpRequest request with the image data.
Tracking upload progress
The following method represents how we can calculate what percentage of an image has been uploaded.
const trackProgress = async (event) => {const newProgress = Math.floor((event.loaded/event.total)*100)setProgress(newProgress);}
As seen on line 2, we’ve divided event.loaded (the total number of bytes of image data uploaded to the server) by event.total (the total actual number of bytes of the image). We multiply the result by 100 to get the percentage.
Backend server
The following code represents the index.js file of the Node server we’ve set up to test the uploading logic of our React application.
// Importing essential librariesimport express from 'express';import multer from 'multer';import cors from 'cors';import { promises as fs } from 'fs';// Initializing the multer and express serverconst upload = multer();const app = express();app.use(cors());// Defining port to be usedconst port = process.env.PORT || 3000;// Defining path where all uploaded images can be viewedapp.all('/', async function(req, res, next) {try {// Reading initial data from upload.json filelet uploads = await fs.readFile('uploads.json');let imageList = JSON.parse(uploads);let imageData = JSON.stringify(imageList);res.setHeader('Content-Type', 'application/json');res.status(200).send(imageData);} catch (err) {res.status(500).send({'message': `Internal Server Error: ${err}`});}next();});// Defining path where all uploaded images are deletedapp.all('/deleteAll', async function(req, res, next) {try {let imageList = {"images":[]};let newData = JSON.stringify(imageList);await fs.writeFile('uploads.json', newData);let imageData = JSON.stringify(imageList);res.setHeader('Content-Type', 'application/json');res.status(200).send(imageData);} catch (err) {res.status(500).send({'message': `Internal Server Error: ${err}`});}next();});// Defining path where the image will be uploadedapp.post('/upload', upload.single("image"), async function(req, res, next) {try {// Reading initial data from upload.json filelet uploads = await fs.readFile('uploads.json');let imageList = JSON.parse(uploads);// Saving image data to uploads.jsonconst base64Data = req.file.buffer.toString('ascii');imageList.images.push({'name': req.file.originalname, 'base64Data': base64Data})let newData = JSON.stringify(imageList);await fs.writeFile('uploads.json', newData);// Handling upload requestif (!req.file) {res.status(400).send('No file uploaded.');} else {res.status(201).send('File uploaded successfully!');}} catch (err) {res.status(500).send({'message': `Internal Server Error: ${err}`});}next();});// Starting Express Serverapp.listen(port);console.log(`Server started at http://localhost:${port}`);
This server will handle the following requests from the React Native application:
Uploading an image
Fetching all uploaded images
Deleting all uploaded images
We implement a crude implementation of image storage, where we simply store the image name and the corresponding image data in a JSON file called uploads.json.
Other endpoints
We’ve also implemented two separate endpoints in our React application apart from uploading images to test and better understand how a complete React Native application will work with an image uploader.
The following code defines our fetchUploadedImages method, which, as the name suggests, will fetch all uploaded images on the server.
const fetchUploadedImages = async () => {setImagesLoading(true);let options = {method: 'GET',headers: {'Content-Type': `application/json`,},}const response = await fetch('{{EDUCATIVE_LIVE_VM_URL}}:3000', options);if (response.ok) {try {const content = await response.json();if (content.images.length === 0){setListImages(<Text>{'No Images Uploaded'}</Text>);} else {const newlistImages = content.images.map((object) => {return (<React.Fragment key={object.name}><View style={{padding: '10px'}} /><View style={{borderWidth: 1, borderRadius: 10, borderColor: 'grey', padding: '5px', alignItems: 'center', justifyContent: 'center' }}><img src={`${object.base64Data}`} alt={`${object.name}`} width="200"/><Text>{`${object.name}`}</Text></View></React.Fragment>);});setListImages(newlistImages);}} catch(_) {console.error(`Error retrieving files`);} finally {setImagesLoading(false);}} else {setImagesLoading(false);console.error(`Error retrieving files`);}}
The following code defines our deleteAllUploadedImages method, which, as the name suggests, will delete all uploaded images on the server.
const deleteAllUploadedImages = async () => {setImagesLoading(true);let options = {method: 'POST'}await fetch('{{EDUCATIVE_LIVE_VM_URL}}:3000/deleteAll', options);await fetchUploadedImages();}
After deletion, the deleteAllUploadedImages method will trigger fetchUploadedImages method to refresh the updated images list.
Rendering logic
The following JSX HTML defines the rendering logic of the image upload page:
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}><View style={{ padding: '5px' }} /><View>{image && <Image source={{ uri: image.uri }} style={{ width: 400, height: 400 }} />}{!image && <Image source={imgPlaceholder} style={{ width: 400, height: 400 }} />}<View style={{ padding: '5px' }} /><Button title="Pick and upload an image" onPress={pickImage} disabled={loading || imagesLoading} /></View>{image &&<View style={{ padding: '20px', alignItems: 'center' }}><Text style={{fontSize: '24px'}}>{ loading ? 'Image Uploading...' : 'Image Uploaded' } {progress}%</Text><View style={{ padding: '5px' }} /><View><ProgressBar progress={progress/100} width={400} height={20} /></View></View>}<View style={{ padding: '20px' }} /><Button title='Fetch Uploaded Images' onPress={fetchUploadedImages} disabled={loading || imagesLoading} />{!imagesLoading && listImages}{imagesLoading && <ActivityIndicator size="large" />}<View style={{ padding: '20px' }}><Button title='Clear Image List' onPress={deleteAllUploadedImages} color='#ff0000' disabled={loading || imagesLoading} /></View></View>
Demo application
Here’s the complete application you can use to test out and play around with the data validation logic we’ve implemented for the sign-up process. Simply click the “Run” button to execute the code. You need to wait a while for the code to compile. After that, you can click on the link in front of “Your app can be found at:” or open the output tab of the widget to open the React Native application.
// Importing essential libraries
import express from 'express';
import multer from 'multer';
import cors from 'cors';
import { promises as fs } from 'fs';
// Initializing the multer and express server
const upload = multer();
const app = express();
app.use(cors());
// Defining port to be used
const port = process.env.PORT || 3000;
// Defining path where all uploaded images can be viewed
app.all('/', async function(req, res, next) {
try {
// Reading initial data from upload.json file
let uploads = await fs.readFile('uploads.json');
let imageList = JSON.parse(uploads);
let imageData = JSON.stringify(imageList);
res.setHeader('Content-Type', 'application/json');
res.status(200).send(imageData);
} catch (err) {
res.status(500).send({'message': `Internal Server Error: ${err}`});
}
next();
});
// Defining path where all uploaded images are deleted
app.all('/deleteAll', async function(req, res, next) {
try {
let imageList = {"images":[]};
let newData = JSON.stringify(imageList);
await fs.writeFile('uploads.json', newData);
let imageData = JSON.stringify(imageList);
res.setHeader('Content-Type', 'application/json');
res.status(200).send(imageData);
} catch (err) {
res.status(500).send({'message': `Internal Server Error: ${err}`});
}
next();
});
// Defining path where the image will be uploaded
app.post('/upload', upload.single("image"), async function(req, res, next) {
try {
// Reading initial data from upload.json file
let uploads = await fs.readFile('uploads.json');
let imageList = JSON.parse(uploads);
// Saving image data to uploads.json
const base64Data = req.file.buffer.toString('ascii');
imageList.images.push({'name': req.file.originalname, 'base64Data': base64Data})
let newData = JSON.stringify(imageList);
await fs.writeFile('uploads.json', newData);
// Handling upload request
if (!req.file) {
res.status(400).send('No file uploaded.');
} else {
res.status(201).send('File uploaded successfully!');
}
} catch (err) {
res.status(500).send({'message': `Internal Server Error: ${err}`});
}
next();
});
// Starting Express Server
app.listen(port);
console.log(`Server started at http://localhost:${port}`);
Free Resources