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