How To Use React Native Expo Image Picker and Firebase File Upload

Aaron K Saunders - Jun 10 '23 - - Dev Community

Overview

This video demonstrates a simple React Native Expo application that integrates Firebase Storage and Expo Image Picker to capture and upload images to Firebase Storage.

We also included the react-native-dotenv npm package to manage our firebase environment variables.

It showcases the use of Firebase SDK functions, React Hooks, and the Expo Image Picker library to interact with the device's camera and Firebase Storage.

Below is additional information in the final files that were create to build the application and steps to set it all

Create Project and Install npm Packages



# create project
npx create-expo-app my-app-camera-fb

#install dotenv for env variables
npm install -D react-native-dotenv

# using camera from image picker
npx expo install expo-image-picker

# using firebase javascript API
npx expo install firebase

# ensuring the firebase api is packaged correctly
npx expo customize metro.config.js


Enter fullscreen mode Exit fullscreen mode

Updates to metro.config.js



const { getDefaultConfig } = require('@expo/metro-config');

const defaultConfig = getDefaultConfig(__dirname);
defaultConfig.resolver.assetExts.push('cjs');

module.exports = defaultConfig;


Enter fullscreen mode Exit fullscreen mode

.env File Setup and Configuration

you need to update the babel.config.js, see the addition to the plugins property



module.exports = function(api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    "plugins": [
      ["module:react-native-dotenv"]
    ]
  };
};


Enter fullscreen mode Exit fullscreen mode

you also need to create a .env file in the root of you project to hold the firebase keys needed for application



FIREBASE_API_KEY=""
FIREBASE_AUTH_DOMAIN=""
FIREBASE_DATABASE_URL=
FIREBASE_PROJECT_ID=""
FIREBASE_STORAGE_BUCKET=""
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=""


Enter fullscreen mode Exit fullscreen mode

Permissions Configuration

In app.json you need to set information for ios plist



"plugins": [
      [
        "expo-image-picker",
        {
          "photosPermission": "Allow $(PRODUCT_NAME) to access your photo",
          "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera"
        }
      ]
    ]


Enter fullscreen mode Exit fullscreen mode

Firebase Code

This code snippet is an example of how to use Firebase Storage in a JavaScript or TypeScript project. Let's go through it step by step:

The code begins by importing specific variables from the @env package. This package is commonly used to store environment variables securely.



import {
  FIREBASE_API_KEY,
  FIREBASE_STORAGE_BUCKET,
  FIREBASE_APP_ID,
  FIREBASE_PROJECT_ID,
  FIREBASE_AUTH_DOMAIN,
} from "@env"; 


Enter fullscreen mode Exit fullscreen mode

Next, it imports various functions and objects from the Firebase App and Firebase Storage modules. These modules are part of the Firebase SDK, which provides tools for building Firebase-powered apps.



import { initializeApp, getApp, getApps } from "firebase/app";
import {
  getStorage,
  ref,
  uploadBytesResumable,
  getDownloadURL,
  listAll,
} from "firebase/storage";


Enter fullscreen mode Exit fullscreen mode

The code defines an object called firebaseConfig that contains the configuration options for your Firebase project. These options include the API key, storage bucket, app ID, project ID, and authentication domain. These values are obtained from the Firebase console when setting up a project.



// Initialize Firebase
const firebaseConfig = {
  apiKey: FIREBASE_API_KEY,
  storageBucket: FIREBASE_STORAGE_BUCKET,
  appId: FIREBASE_APP_ID,
  projectId: FIREBASE_PROJECT_ID,
  authDomain: FIREBASE_AUTH_DOMAIN,
};


Enter fullscreen mode Exit fullscreen mode

The console.log(firebaseConfig) line outputs the configuration object to the console for debugging purposes.

The if (getApps().length === 0) condition checks if there are any Firebase apps initialized. This is to ensure that the Firebase app is only initialized once and prevents errors if this code is executed multiple times.

If no Firebase apps are initialized, the initializeApp(firebaseConfig) function is called to initialize the Firebase app using the provided configuration.



if (getApps().length === 0) {
  initializeApp(firebaseConfig);
}


Enter fullscreen mode Exit fullscreen mode

The getApp() function is then used to retrieve the default Firebase app instance, and getStorage() is used to get a reference to the Firebase Storage service. I never actually needed these variables, but it was left in the code to show how they can be exported for use in other parts of application.

The listFiles function is defined as an asynchronous function that lists all the files in a specific Firebase Storage reference. It first creates a reference to the desired location using ref(storage, "images"), where "images" is the path to the directory you want to list files from. It then calls listAll on the reference to retrieve a list of files and directories under that path.



const listFiles = async () => {
  const storage = getStorage();

  // Create a reference under which you want to list
  const listRef = ref(storage, "images");

  // Find all the prefixes and items.
  const listResp = await listAll(listRef);
  return listResp.items;
};


Enter fullscreen mode Exit fullscreen mode

The uploadToFirebase function is defined to upload a file to Firebase Storage. It takes three parameters: uri (the URI of the file to upload), name (the desired name of the uploaded file), and onProgress (an optional callback to track the upload progress). This function fetches the file data from the provided URI, creates a reference to the desired location using ref(getStorage(), "images/${name}"), and then uses uploadBytesResumable to upload the file in chunks. The function returns a Promise that resolves with the download URL and metadata of the uploaded file.



const uploadToFirebase = async (uri, name, onProgress) => {
  const fetchResponse = await fetch(uri);
  const theBlob = await fetchResponse.blob();

  const imageRef = ref(getStorage(), `images/${name}`);

  const uploadTask = uploadBytesResumable(imageRef, theBlob);

  return new Promise((resolve, reject) => {
    uploadTask.on(
      "state_changed",
      (snapshot) => {
        const progress =
          (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        onProgress && onProgress(progress);
      },
      (error) => {
        // Handle unsuccessful uploads
        console.log(error);
        reject(error);
      },
      async () => {
        const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref);
        resolve({
          downloadUrl,
          metadata: uploadTask.snapshot.metadata,
        });
      }
    );
  });
};


Enter fullscreen mode Exit fullscreen mode

Finally, the code exports the fbApp, fbStorage, uploadToFirebase, and listFiles variables, which can be imported and used in other parts of your application.



export { fbApp, fbStorage, uploadToFirebase, listFiles };


Enter fullscreen mode Exit fullscreen mode

Overall, this code initializes the Firebase app and provides functions for listing files in Firebase Storage and uploading files to Firebase Storage.

Main Application Code

The code imports necessary components and functions from various packages, including expo-status-bar, React Native components, expo-image-picker, Firebase-related modules (listFiles and uploadToFirebase functions from firebase-config.js), and the custom MyFilesList component.

The App function is the main component of the application. It uses React Hooks (useState and useEffect) to manage state and lifecycle events.

We have defined the following code to leverage the useCameraPermissions hook to get current permission and also a function to call to prompt the user for permission, we will reference that later in this module.



const [permission, requestPermission] = ImagePicker.useCameraPermissions();
const [files, setFiles] = useState([]);


Enter fullscreen mode Exit fullscreen mode

Inside the useEffect hook, the code fetches the initial list of files from Firebase Storage when the component mounts. It calls the listFiles function from firebase-config.js to retrieve a list of files. It then maps the response to create an array of file objects with just the name property (extracted from the fullPath property of each file). Finally, it updates the state with the array of files using the setFiles function.

The console.log(files) statement logs the current state of the files array to the console for debugging purposes.

The takePhoto function is an asynchronous function that handles capturing an image using the device's camera. It utilizes the launchCameraAsync function from expo-image-picker to open the camera interface. The function passes some configuration options such as allowing editing, capturing all media types, and setting the image quality to 1. Once an image is captured, it checks if the operation was canceled or not.



  const takePhoto = async () => {
    try {
      const cameraResp = await ImagePicker.launchCameraAsync({
        allowsEditing: true,
        mediaTypes: ImagePicker.MediaTypeOptions.All,
        quality: 1,
      });

      if (!cameraResp.canceled) {
        const { uri } = cameraResp.assets[0];
        const fileName = uri.split("/").pop();
        const uploadResp = await uploadToFirebase(uri, fileName, (v) =>
          console.log(v)
        );
        console.log(uploadResp);

        listFiles().then((listResp) => {
          const files = listResp.map((value) => {
            return { name: value.fullPath };
          });

          setFiles(files);
        });
      }
    } catch (e) {
      Alert.alert("Error Uploading Image " + e.message);
    }
  };


Enter fullscreen mode Exit fullscreen mode

If the image capture was not canceled, it extracts the URI (file path) of the captured image and obtains the file name by splitting the URI and retrieving the last segment.

It then calls the uploadToFirebase function from firebase-config.js, passing the URI, file name, and an optional callback function to track the upload progress. The function uploads the image to Firebase Storage using the Firebase SDK. The returned promise resolves with an object containing the download URL and metadata of the uploaded file. This information is logged to the console for debugging purposes.

After a successful upload, the code fetches the updated list of files from Firebase Storage by calling the listFiles function again. It follows the same process as before, mapping the response to create an array of file objects and updating the state with the new array of files.

If any error occurs during the image capture or upload process, an error message is displayed using Alert.alert function from React Native.

The user should only be able to take pictures if permission in granted; There are some addition configuration actions required for this to work.





Enter fullscreen mode Exit fullscreen mode

The code checks if camera permission is granted by comparing the status property of the permission object obtained from ImagePicker.useCameraPermissions() with ImagePicker.PermissionStatus.GRANTED. If permission is not granted, a view is rendered that displays a message indicating the permission status and a button to request permission.



// permission check
if (permission?.status !== ImagePicker.PermissionStatus.GRANTED) {
    return (
      <View style={styles.container}>
        <Text>Permission Not Granted - {permission?.status}</Text>
        <StatusBar style="auto" />
        <Button title="Request Permission" onPress={requestPermission}></Button>
      </View>
    );
  }


Enter fullscreen mode Exit fullscreen mode

If camera permission is granted, the main UI is rendered. It includes a component displaying a message, the MyFilesList component (which renders the list of files), a component, and a component that triggers the takePhoto function when pressed.



  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.container}>
        <Text>Working With Firebase and Image Picker</Text>
        <MyFilesList files={files} />
        <StatusBar style="auto" />
        <Button title="Take Picture" onPress={takePhoto}></Button>
      </View>
    </SafeAreaView>
  );


Enter fullscreen mode Exit fullscreen mode

Custom MyList Component

This code defines a custom React Native component called MyFilesList. It is used to display a list of files in a flat list format. Here's a breakdown of the code:

The MyFilesList function is the main component. It receives a prop called files, which is an array of file objects.



export default function MyFilesList({ files }) {


Enter fullscreen mode Exit fullscreen mode

Inside the MyFilesList component, there is a nested component called Item. This component represents each item in the list and receives a prop called name, which represents the name of the file.

The Item component renders a View component with a custom style (styles.item). Inside the view, there is a Text component that displays the file name using the name prop. The text style is defined by the styles.title style.



  const Item = ({ name }) => (
    <View style={styles.item}>
      <Text style={styles.title}>{name}</Text>
    </View>
  );


Enter fullscreen mode Exit fullscreen mode

The MyFilesList component returns a FlatList component from React Native. The FlatList is the main component responsible for rendering the list of files.



  return (
    <FlatList
      data={files}
      renderItem={({ item }) => <Item name={item.name} />}
      keyExtractor={(item) => item.name}
    />
  );


Enter fullscreen mode Exit fullscreen mode

The FlatList component receives several props:

  • data is set to the files prop, which represents the array of file objects.
  • renderItem is a callback function that renders each item in the list. It receives the item object, and in this case, it renders the Item component and passes the name property of the item object as the name prop of the Item component.
  • keyExtractor is a function that extracts a unique key for each item in the list. In this case, it uses the name property of each item as the key.

Finally, the code defines a styles object using StyleSheet.create(). It defines two styles: item and title. The item style sets margin vertical and horizontal spacing, and the title style sets the font size to 18.

This code provides a reusable component MyFilesList that takes an array of files and displays them in a flat list format. It demonstrates the usage of React Native's FlatList component, custom nested components, and styles defined using StyleSheet.create().

Full Project In GitHub

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .