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:

  1. 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-picker image picker library provided by Expo.

  2. Uploading the image: This task involves uploading the image to a backend server. We’ll use XMLHttpRequest API, which is supported by all modern browsers, to send an image file to a web server through HTTP requests.

  3. Showing progress bar for image upload: This task involves adding a listener on the XMLHttpRequest request to track what percentage of the total bytes of the image has been uploaded. We’ll use the react-native-progress library 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-picker
npx expo install expo-image-picker
# Installing react-native-progress
npm 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 image
try {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
base64: true,
aspect: [4, 3],
quality: 1,
});
// Selecting the picked image
if (!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 URI
const imageUri = image.uri;
// Creating new file name
const 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 boundary
const 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 request
const request = new XMLHttpRequest();
// Adding listeners to monitor upload progress
request.upload.addEventListener('progress', trackProgress);
request.addEventListener('load', () => {
setProgress(100);
});
// Making HTTP request
request.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 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}`);

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}`);
React Native Application: Image Picker with upload progress and gallery display

Free Resources

Copyright ©2025 Educative, Inc. All rights reserved