How to implement Image Upload with Expo Image Picker in React Native for a professional app launched app.

How to implement Image Upload with Expo Image Picker in React Native for a professional app launched app.

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?

Personalisation is a key factor in enhancing user engagement within an app. Allowing users to upload a profile image not only personalises 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 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.

  1. Installing Required Packages
  2. Requesting Permissions
  3. Allowing Users to Pick an Image
  4. Uploading the Image to the Backend
  5. 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.

installbash
1expo 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.

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 users 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.

1const pickImage = async () => {
2  // Ask for permission to access the gallery
3  const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
4  if (status !== 'granted') {
5    Alert.alert(
6      'Permission Denied',
7      'Permission to access gallery was denied'
8    );
9    return;
10  }
11
12  // Launch the image picker
13  const result = await ImagePicker.launchImageLibraryAsync({
14    mediaTypes: ImagePicker.MediaTypeOptions.Images,
15    allowsEditing: true,
16    aspect: [1, 1],
17    quality: 1,
18    base64: true, // Include base64 in the result
19  });
20
21  if (!result.canceled) {
22    setLoadingImage(true);
23    const completeBase64String =
24      'data:image/png;base64,' + result.assets[0].base64;
25    uploadProfilePicture(completeBase64String, result.assets[0].uri);
26  } else {
27    setLoadingImage(false);
28    Alert.alert('Error', 'Failed to select image');
29  }
30};

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 users 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.

1const completeBase64String =
2      'data:image/png;base64,' + result.assets[0].base64;
3    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.

1const uploadProfilePicture = async (base64Image, uri) => {
2  const endpoint = 'user';
3  const method = 'PUT'; // Use PUT as per your requirement
4
5  try {
6    // Prepare the data object
7    const data = {
8      profilePicture: base64Image,
9    };
10
11    // Make the API request
12    const response = await actionProvider.makeRequest(endpoint, method, data);
13
14    if (response.status === 200) {
15      Alert.alert('Success', 'Profile picture updated successfully');
16      // Optionally, refresh user information
17      fetchUserInformation(session);
18      setLoadingImage(false);
19      setSelectedImage(uri);
20    } else {
21      Alert.alert('Error', 'Failed to update profile picture');
22      setLoadingImage(false);
23    }
24  } catch (error) {
25    setLoadingImage(false);
26    console.error('Error uploading profile picture:', error);
27    Alert.alert(
28      'Error',
29      'An error occurred while uploading the profile picture'
30    );
31  }
32};

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., using fetch or axios). 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.

1<View style={styles.profileImageContainer}>
2  <TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
3    {loadingImage ? (
4      <View
5        style={[
6          styles.profileImage,
7          {
8            justifyContent: 'center',
9            alignItems: 'center',
10          },
11        ]}
12      >
13        <ActivityIndicator color={'#4096C1'} />
14      </View>
15    ) : selectedImage ? (
16      <>
17        <Image
18          source={{ uri: selectedImage }}
19          style={styles.profileImage}
20          resizeMode='cover'
21        />
22      </>
23    ) : (
24      <Image
25        source={require('@/assets/images/profile/profileImage.png')}
26        style={styles.profileImage}
27        resizeMode='contain'
28      />
29    )}
30  </TouchableOpacity>
31</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.

index.tsxtsx
1import AnimatedWrapper from "@/components/shared/animation";
2import GlobalButton from "@/components/shared/Button";
3import { Colors, globalStyle } from "@/constants/Colors";
4import { useSession } from "@/providers/Auth/AuthProvider";
5import React, { useCallback, useEffect, useState } from "react";
6import {
7  View,
8  Text,
9  StyleSheet,
10  Image,
11  ImageBackground,
12  TouchableOpacity,
13  ScrollView,
14  RefreshControl,
15  Alert,
16  ActivityIndicator,
17} from "react-native";
18import Subscription from "../../Subscription";
19
20import * as ImagePicker from "expo-image-picker";
21import actionProvider from "../../../../providers/actionProvider";
22import { useToast } from "../../../../providers/useToast";
23
24const HEIGHT = 80;
25
26const index = () => {
27  const { user, session, fetchUserInformation } = useSession();
28
29  const [modalVisible, setModalVisible] = useState(false);
30
31  const [loading, setLoading] = React.useState(false);
32  const [loadingImage, setLoadingImage] = useState(false);
33
34  const [selectedImage, setSelectedImage] = useState("");
35
36  const { showToast } = useToast();
37
38  useEffect(() => {
39    // If user has a profile picture, set it
40    if (user?.profilePicture) {
41      setSelectedImage(user.profilePicture);
42    }
43  }, [user]);
44
45  const onRefresh = useCallback(async () => {
46    // Start refreshing
47    setLoading(true);
48    fetchUserInformation(session);
49
50    // Simulate a network request or any async action
51    setTimeout(() => {
52      // Stop refreshing after 3 seconds
53      setLoading(false);
54    }, 3000);
55  }, []);
56
57  const pickImage = async () => {
58    // Ask for permission to access the gallery
59    const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
60    if (status !== "granted") {
61      Alert.alert(
62        "Permission Denied",
63        "Permission to access gallery was denied"
64      );
65      return;
66    }
67
68    // Launch the image picker
69    const result = await ImagePicker.launchImageLibraryAsync({
70      mediaTypes: ImagePicker.MediaTypeOptions.Images,
71      allowsEditing: true,
72      aspect: [1, 1],
73      quality: 1,
74      base64: true, // Include base64 in the result
75    });
76
77    const completeBase64String =
78      "data:image/png;base64," + result?.assets[0]?.base64;
79
80    if (!result.cancelled) {
81      setLoadingImage(true);
82      uploadProfilePicture(completeBase64String, result?.assets[0].uri);
83    } else {
84      setLoadingImage(false);
85
86      showToast("error", "Failed to update profile picture");
87    }
88  };
89
90  const uploadProfilePicture = async (base64Image: string, uri: string) => {
91    const endpoint = "user";
92    const method = "PUT"; // Use PUT as per your requirement
93
94    try {
95      // Prepare the data object
96      const data = {
97        profilePicture: base64Image,
98      };
99
100      // Make the API request
101      const response = await actionProvider.makeRequest(endpoint, method, data);
102
103      if (response.status === 200) {
104        Alert.alert("Success", "Profile picture updated successfully");
105        // Optionally, refresh user information
106        fetchUserInformation(session);
107        setLoadingImage(false);
108
109        setSelectedImage(uri);
110      } else {
111        showToast("error", "Failed to update profile picture");
112        setLoadingImage(false);
113      }
114    } catch (error) {
115      setLoadingImage(false);
116
117      console.error("Error uploading profile picture:", error);
118      showToast(
119        "error",
120        "An error occurred while uploading the profile picture"
121      );
122    }
123  };
124
125  return (
126    <View
127      style={{
128        flex: 1,
129        backgroundColor: "white",
130      }}
131    >
132      <ScrollView
133        refreshControl={
134          <RefreshControl
135            refreshing={loading}
136            onRefresh={onRefresh}
137            colors={["#4096C1"]} // Customize spinner colors
138            tintColor="#4096C1" // Customize spinner color for iOS
139          />
140        }
141        contentContainerStyle={{
142          flex: 1,
143        }}
144      >
145        <View style={styles.container}>
146          <ImageBackground
147            style={styles.innerContainer}
148            source={require("@/assets/images/profile/profilePageImage.png")}
149            imageStyle={styles.backgroundImage}
150          >
151            <View style={styles.profileImageContainer}>
152              {/* This is the profile image section */}
153              <TouchableOpacity onPress={pickImage} activeOpacity={0.8}>
154                {loadingImage ? (
155                  <View
156                    style={[
157                      styles.profileImage,
158                      {
159                        justifyContent: "center",
160                        alignItems: "center",
161                      },
162                    ]}
163                  >
164                    <ActivityIndicator color={"#4096C1"} />
165                  </View>
166                ) : selectedImage ? (
167                  <>
168                    <Image
169                      source={{ uri: selectedImage }}
170                      style={styles.profileImage}
171                      resizeMode="cover"
172                    />
173                  </>
174                ) : (
175                  <Image
176                    source={require("@/assets/images/profile/profileImage.png")}
177                    style={styles.profileImage}
178                    resizeMode="contain"
179                  />
180                )}
181              </TouchableOpacity>
182
183              {/* This is the profile image section */}
184            </View>
185          </ImageBackground>
186        </View>
187      </ScrollView>
188    </View>
189  );
190};
191
192export default index;
193
194const styles = StyleSheet.create({
195  container: {
196    flex: 1,
197    backgroundColor: "white",
198  },
199  innerContainer: {
200    height: 200,
201    backgroundColor: Colors.button.backgroundButtonDark,
202    borderBottomLeftRadius: 40,
203    borderBottomRightRadius: 40,
204    alignItems: "center",
205    justifyContent: "flex-end",
206  },
207  profileImageContainer: {
208    position: "absolute",
209    bottom: -HEIGHT / 2,
210    height: HEIGHT,
211    width: HEIGHT,
212    borderRadius: HEIGHT / 2,
213    backgroundColor: "white",
214    padding: 5,
215    justifyContent: "center",
216    alignItems: "center",
217  },
218  backgroundImage: {
219    borderBottomLeftRadius: 40,
220    borderBottomRightRadius: 40,
221  },
222  profileImage: {
223    height: HEIGHT / 1.2,
224    width: HEIGHT / 1.2,
225    borderRadius: HEIGHT / 1.5,
226  },
227  infoContainer: {
228    alignItems: "center",
229    marginTop: 30,
230    flex: 1,
231  },
232  greetingText: {
233    fontSize: 20,
234    fontWeight: "bold",
235    fontFamily: globalStyle.font.fontFamilyBold,
236    textAlign: "center",
237  },
238  planContainer: {
239    marginVertical: 10,
240    backgroundColor: "rgba(64,150,193,.20)",
241    borderRadius: 5,
242    paddingVertical: 5,
243    paddingHorizontal: 20,
244  },
245  planText: {
246    color: "#000000",
247    fontFamily: globalStyle.font.fontFamilyBold,
248  },
249  buttonContainer: {
250    marginHorizontal: globalStyle.container.marginHorizontal,
251    bottom: 10,
252  },
253  premiumContainer: {
254    marginTop: 30,
255    width: "90%",
256    justifyContent: "space-between",
257    alignItems: "center",
258    backgroundColor: "#EFF6FA",
259    flexDirection: "row",
260    paddingVertical: 15,
261    paddingHorizontal: 10,
262    borderRadius: 10,
263  },
264  premiumContent: {
265    flexDirection: "row",
266    alignItems: "center",
267    flex: 0.8,
268  },
269  premiumIcon: {
270    height: 30,
271    width: 30,
272  },
273  premiumText: {
274    fontFamily: globalStyle.font.fontFamilyBold,
275    fontSize: 16,
276    marginLeft: 15,
277  },
278  upgradeText: {
279    fontFamily: globalStyle.font.fontFamilyBold,
280    color: Colors.button.textDark,
281  },
282});

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!

Drag