Introduction
At DETL, we specialize in crafting stellar software applications that seamlessly blend hardcore engineering with sleek design. Our mission is to deliver both mobile and web applications that provide exceptional user experiences without compromising on aesthetics or functionality. In this blog post, we will guide you through how to implement an image picker in your Expo React Native application. This feature will allow users to upload a profile image, adding a personal touch to their profiles.
Why Implement a Profile Image Picker?
Personalization is a key factor in enhancing user engagement within an app. Allowing users to upload a profile image not only personalizes their experience but also creates a deeper connection with the application. In this blog post, we’ll show you how to implement this feature using Expo’s ImagePicker
, enabling users to select and upload profile pictures seamlessly.
Overview
Before embarking on this journey of explaining to you how to allow users to upload their profile image, I’d like to first break down the topics just so you’ll have a solid idea of what we will be covering in this blog post.
- Installing Required Packages
- Requesting Permissions
- Allowing Users to Pick an Image
- Uploading the Image to the Backend
- Updating the User’s Profile Picture
We will explain each step in detail so you are not left confused :)
Step 1: Installing Required Packages
Before we start, make sure you have Expo React Native application configured so that you can install the following package called expo-image-picker
.
expo install expo-image-picker
Step 2: Importing Necessary Modules and Setting Up State
In your profile component file, import the necessary package and as well as create the necessary states that will handle the uploading of the image.
We have a folder called profile -> index.tsx
and we place the code which we will show you below inside of the index.tsx
file.
import React, { useCallback, useEffect, useState } from 'react';
import {
View,
Text,
StyleSheet,
Image,
TouchableOpacity,
ScrollView,
RefreshControl,
Alert,
ActivityIndicator,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
// State variables
const [selectedImage, setSelectedImage] = useState('');
const [loadingImage, setLoadingImage] = useState(false);
We have declared two states, the first state saves the selected image and the second state is a loading state which we will use when we are making an API call to our backend to save the image to the user's profile. We also import expo-image-picker
.
Step 3: Requesting Permissions and Picking an Image
We need to request permission to access the user’s media library and then allow them to select an image.
const pickImage = async () => {
// Ask for permission to access the gallery
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'Permission Denied',
'Permission to access gallery was denied'
);
return;
}
// Launch the image picker
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
base64: true, // Include base64 in the result
});
if (!result.canceled) {
setLoadingImage(true);
const completeBase64String =
'data:image/png;base64,' + result.assets[0].base64;
uploadProfilePicture(completeBase64String, result.assets[0].uri);
} else {
setLoadingImage(false);
Alert.alert('Error', 'Failed to select image');
}
};
When we request access from the user we check if the status was granted; if not, then we throw an alert. Once the status has been granted, the app will automatically navigate to the user's media library where their images are stored.
Firstly, it is important to note that when we pick the image the user has selected, we make sure that we have access to the base64
format of the image since that is what we will be sending back to the backend when we make our API call.
Secondly, as you can see from the code as shown below is that we attach a string to the result.
const completeBase64String =
'data:image/png;base64,' + result.assets[0].base64;
uploadProfilePicture(completeBase64String, result.assets[0].uri);
The selected image returns a base64 without the first part of the image data:image/png;base64,
which is important, otherwise our backend will not be able to process the image correctly. What we do is attach a base64 image string to the result we get from the image picker so that our backend is able to process the image accordingly. This is a specific use case and can as well be handled by your backend but it is much easier in its implementation on the frontend.
So far we have selected our image, received the equivalent base64 result of that image and now we will make a backend call with the base64 result of the image which is shown in the above code as such uploadProfilePicture(completeBase64String, result.assets[0].uri)
.
Step 4: Uploading the Image to the Backend
We need to send the selected image to the backend server to update the user’s object. Whenever the user is re-authenticated, the image is then fetched from their profile and displayed on the app.
const uploadProfilePicture = async (base64Image, uri) => {
const endpoint = 'user';
const method = 'PUT'; // Use PUT as per your requirement
try {
// Prepare the data object
const data = {
profilePicture: base64Image,
};
// Make the API request
const response = await actionProvider.makeRequest(endpoint, method, data);
if (response.status === 200) {
Alert.alert('Success', 'Profile picture updated successfully');
// Optionally, refresh user information
fetchUserInformation(session);
setLoadingImage(false);
setSelectedImage(uri);
} else {
Alert.alert('Error', 'Failed to update profile picture');
setLoadingImage(false);
}
} catch (error) {
setLoadingImage(false);
console.error('Error uploading profile picture:', error);
Alert.alert(
'Error',
'An error occurred while uploading the profile picture'
);
}
};
In simple steps we will explain to you what is happening in the code above.
Data Preparation: We create a data object containing the base64 image which we will send as a payload within the API call.
API Call: We make a PUT request to the backend API to update the user’s profile picture.
Note: Replace
actionProvider.makeRequest
with your actual API endpoint method (e.g., usingfetch
oraxios
). Since we have a hooks file where we make all of our API calls, we have placed our endpoint URL in this file so that we can import it wherever we need to make an API call.Handling Responses: We check the response status and update the UI accordingly.
Step 5: Displaying the Profile Image
We update the UI to display the user’s profile image or a placeholder if none is set.
<View style={styles.profileImageContainer}>
<TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
{loadingImage ? (
<View
style={[
styles.profileImage,
{
justifyContent: 'center',
alignItems: 'center',
},
]}
>
<ActivityIndicator color={'#4096C1'} />
</View>
) : selectedImage ? (
<>
<Image
source={{ uri: selectedImage }}
style={styles.profileImage}
resizeMode='cover'
/>
</>
) : (
<Image
source={require('@/assets/images/profile/profileImage.png')}
style={styles.profileImage}
resizeMode='contain'
/>
)}
</TouchableOpacity>
</View>
TouchableOpacity: Makes the image clickable, allowing users to change their profile picture which runs the
pickImage
function to show the media library.Loading Indicator: Shows while the backend API call is made and the image is uploading.
Selected Image: Displays the user’s chosen profile picture.
Placeholder Image: Shown if no profile picture is set. This can be stored in a file inside your app like the
public → assets → images → profile → profileImage.png
folder.
Entire Code
Below we have posted the entire code. This way you can have a bird-eye view of the entire implementation and as well as follow the steps outlined above in a much more thorough way.
import AnimatedWrapper from "@/components/shared/animation";
import GlobalButton from "@/components/shared/Button";
import { Colors, globalStyle } from "@/constants/Colors";
import { useSession } from "@/providers/Auth/AuthProvider";
import React, { useCallback, useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
Image,
ImageBackground,
TouchableOpacity,
ScrollView,
RefreshControl,
Alert,
ActivityIndicator,
} from "react-native";
import Subscription from "../../Subscription";
import * as ImagePicker from "expo-image-picker";
import actionProvider from "../../../../providers/actionProvider";
import { useToast } from "../../../../providers/useToast";
const HEIGHT = 80;
const index = () => {
const { user, session, fetchUserInformation } = useSession();
const [modalVisible, setModalVisible] = useState(false);
const [loading, setLoading] = React.useState(false);
const [loadingImage, setLoadingImage] = useState(false);
const [selectedImage, setSelectedImage] = useState("");
const { showToast } = useToast();
useEffect(() => {
// If user has a profile picture, set it
if (user?.profilePicture) {
setSelectedImage(user.profilePicture);
}
}, [user]);
const onRefresh = useCallback(async () => {
// Start refreshing
setLoading(true);
fetchUserInformation(session);
// Simulate a network request or any async action
setTimeout(() => {
// Stop refreshing after 3 seconds
setLoading(false);
}, 3000);
}, []);
const pickImage = async () => {
// Ask for permission to access the gallery
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Permission Denied",
"Permission to access gallery was denied"
);
return;
}
// Launch the image picker
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
base64: true, // Include base64 in the result
});
const completeBase64String =
"data:image/png;base64," + result?.assets[0]?.base64;
if (!result.cancelled) {
setLoadingImage(true);
uploadProfilePicture(completeBase64String, result?.assets[0].uri);
} else {
setLoadingImage(false);
showToast("error", "Failed to update profile picture");
}
};
const uploadProfilePicture = async (base64Image: string, uri: string) => {
const endpoint = "user";
const method = "PUT"; // Use PUT as per your requirement
try {
// Prepare the data object
const data = {
profilePicture: base64Image,
};
// Make the API request
const response = await actionProvider.makeRequest(endpoint, method, data);
if (response.status === 200) {
Alert.alert("Success", "Profile picture updated successfully");
// Optionally, refresh user information
fetchUserInformation(session);
setLoadingImage(false);
setSelectedImage(uri);
} else {
showToast("error", "Failed to update profile picture");
setLoadingImage(false);
}
} catch (error) {
setLoadingImage(false);
console.error("Error uploading profile picture:", error);
showToast(
"error",
"An error occurred while uploading the profile picture"
);
}
};
return (
<View
style={{
flex: 1,
backgroundColor: "white",
}}
>
<ScrollView
refreshControl={
<RefreshControl
refreshing={loading}
onRefresh={onRefresh}
colors={["#4096C1"]} // Customize spinner colors
tintColor="#4096C1" // Customize spinner color for iOS
/>
}
contentContainerStyle={{
flex: 1,
}}
>
<View style={styles.container}>
<ImageBackground
style={styles.innerContainer}
source={require("@/assets/images/profile/profilePageImage.png")}
imageStyle={styles.backgroundImage}
>
<View style={styles.profileImageContainer}>
{/* This is the profile image section */}
<TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
{loadingImage ? (
<View
style={[
styles.profileImage,
{
justifyContent: "center",
alignItems: "center",
},
]}
>
<ActivityIndicator color={"#4096C1"} />
</View>
) : selectedImage ? (
<>
<Image
source={{ uri: selectedImage }}
style={styles.profileImage}
resizeMode="cover"
/>
</>
) : (
<Image
source={require("@/assets/images/profile/profileImage.png")}
style={styles.profileImage}
resizeMode="contain"
/>
)}
</TouchableOpacity>
{/* This is the profile image section */}
</View>
</ImageBackground>
</View>
</ScrollView>
</View>
);
};
export default index;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "white",
},
innerContainer: {
height: 200,
backgroundColor: Colors.button.backgroundButtonDark,
borderBottomLeftRadius: 40,
borderBottomRightRadius: 40,
alignItems: "center",
justifyContent: "flex-end",
},
profileImageContainer: {
position: "absolute",
bottom: -HEIGHT / 2,
height: HEIGHT,
width: HEIGHT,
borderRadius: HEIGHT / 2,
backgroundColor: "white",
padding: 5,
justifyContent: "center",
alignItems: "center",
},
backgroundImage: {
borderBottomLeftRadius: 40,
borderBottomRightRadius: 40,
},
profileImage: {
height: HEIGHT / 1.2,
width: HEIGHT / 1.2,
borderRadius: HEIGHT / 1.5,
},
infoContainer: {
alignItems: "center",
marginTop: 30,
flex: 1,
},
greetingText: {
fontSize: 20,
fontWeight: "bold",
fontFamily: globalStyle.font.fontFamilyBold,
textAlign: "center",
},
planContainer: {
marginVertical: 10,
backgroundColor: "rgba(64,150,193,.20)",
borderRadius: 5,
paddingVertical: 5,
paddingHorizontal: 20,
},
planText: {
color: "#000000",
fontFamily: globalStyle.font.fontFamilyBold,
},
buttonContainer: {
marginHorizontal: globalStyle.container.marginHorizontal,
bottom: 10,
},
premiumContainer: {
marginTop: 30,
width: "90%",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: "#EFF6FA",
flexDirection: "row",
paddingVertical: 15,
paddingHorizontal: 10,
borderRadius: 10,
},
premiumContent: {
flexDirection: "row",
alignItems: "center",
flex: 0.8,
},
premiumIcon: {
height: 30,
width: 30,
},
premiumText: {
fontFamily: globalStyle.font.fontFamilyBold,
fontSize: 16,
marginLeft: 15,
},
upgradeText: {
fontFamily: globalStyle.font.fontFamilyBold,
color: Colors.button.textDark,
},
});
Conclusion
In this blog post, we’ve demonstrated how to implement an image picker in your Expo React Native application to allow users to upload and update their profile images. This feature enhances user engagement by adding a personalized touch to their profiles.
If there is anything unclear in this blog post, feel free to reach out to DETL and we will be happy to help you through the journey of implementing this.
Happy Coding!
Top comments (0)