Upload images in React Native apps using Firebase and Firestore

With React Native, you can build cross-platform mobile applications using JavaScript as the primary programming language. Each of your mobile apps may contain single or multiple user interfaces to serve a wide range of purposes.

One such user interface is to upload images. In this tutorial, we’ll build a small demo app that allows the user to upload an image with associated details to Firebase’s real-time database Firestore and create a collection called posts. This collection of posts for the demo app will consist of an image and its title.

At the same time, by querying the database, the application will be able to fetch multiple posts in one request. Apart from that, we’ll familiarize myself with the react-native-ui-kitten library. It has a great set of ready-to-use components that help decrease needed amounts of development time.

Table of Contents

  • Requirements
  • Setting up UI Kitten
  • Creating a tab navigator
  • Adding icons to tab bar
  • Mocking data in a feed screen
  • Using high order functions to add styles
  • Add Firebase queries
  • Using Context API
  • Upload images to Firestore
  • Fetching posts from Firestore
  • Conclusion

Stack/Requirements

Setting up UI Kitten

Before focusing on the rest of the tutorial, please make sure you have the following dependencies installed in your React Native project. Follow the commands in the sequence in which they’re presented below:

react-native init rnFirestoreUploadExample

# after the project directory is created
cd rnFirestoreUploadExample

# install the following

yarn add react-navigation react-native-svg [email protected] react-native-gesture-handler react-native-reanimated react-navigation-tabs react-native-ui-kitten @eva-design/eva @ui-kitten/eva-icons uuid react-native-image-picker react-native-firebase

# for ios

I’m using the latest version of react-native-cli at the time of writing this post, with react-native version 0.61.4.

react-native-ui-kitten does provide interactive documentation. Make sure to configure the application root from the docs here just to verify that its related dependencies have been installed correctly.

The UI Kitten library provides a default light and dark theme that can be switched between. Modify the App.js file as per the above code snippet to configure the application root.

import React from 'react'
import { mapping, light as lightTheme } from '@eva-design/eva'
import { ApplicationProvider, Layout, Text } from 'react-native-ui-kitten'

const ApplicationContent = () => (
  <Layout style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
    <Text>Welcome to UI Kitten</Text>
  </Layout>
)

const App = () => (
  <ApplicationProvider mapping={mapping} theme={lightTheme}>
    <ApplicationContent />
  </ApplicationProvider>
)

export default App

You’ll have to open two tabs in your terminal window to run the app at this stage.

# in the first window, run:
yarn start

# in the second window, depending on your development OS
react-native run-ios

# or

react-native run-android

And you’ll get the following output at this stage in an emulator.

I’m not going to dwell on setting up the react-navigation library here. To integrate it, please follow the appropriate set of instructions depending on your react-native version from here.

Creating a tab navigator

The demo app will contain two screens. The first is to show users all the posts uploaded to Firestore, and the second screen is to allow users to upload their post. Each post will contain the image and its title.

Let’s implement these two screens in this step and display some mock data for now. Create a src/screens directory and inside it create two screen component files: Feed.js and AddPost.js.

Open the Feed.js file and add the following:

import React from 'react'
import { Text, Layout } from 'react-native-ui-kitten'

const Feed = () => (
  <Layout style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
    <Text>Feed Screen</Text>
  </Layout>
)

export default Feed

Next, open the AddPost.js file to add the following code snippet:

import React from 'react'..
import { Text, Layout } from 'react-native-ui-kitten'

const AddPost = () => (
 <Layout style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
 <Text>AddPost Screen</Text>
 </Layout>
)

export default AddPost

Next, create a new file named TabNavigator.js inside the src/navigation directory. Since the 4.x version of the react-navigation library, all navigation patterns are separated in their npm packages. Import the required libraries and both the screen components.

import React from 'react'
import { createAppContainer } from 'react-navigation'
import { createBottomTabNavigator } from 'react-navigation-tabs'

import Feed from '../screens/Feed'
import AddPost from '../screens/AddPost'

Add the following route config to create a simple tab bar.

const TabNavigator = createBottomTabNavigator({
  Feed: {
    screen: Feed
  },
  AddPost: {
    screen: AddPost
  }
})

export default createAppContainer(TabNavigator)

Using react-navigation, routes are lazily initialized by default. This means any screen component is not mounted until it becomes active first.

To integrate this tab navigator, open the App.js file and modify it:

import React from 'react'
import { mapping, light as lightTheme } from '@eva-design/eva'
import { ApplicationProvider } from 'react-native-ui-kitten'

import TabNavigator from './src/navigation/TabNavigator'

const App = () => (
  <ApplicationProvider mapping={mapping} theme={lightTheme}>
    <TabNavigator />
  </ApplicationProvider>
)

export default App

The tab bar displays the name of the active screen component in blue by default. Here’s the output:

Adding icons to the tab bar

Instead of displaying names for each screen, we’ll display the appropriate icons. We’ve already installed the icon library. Modify the App.js file to integrate icons from @ui-kitten/eva-icons, which can be configured using IconRegistery.

Do note that this library depends on react-native-svg, so you have to follow the instructions here on how to install that.

import React, { Fragment } from 'react'
import { mapping, light as lightTheme } from '@eva-design/eva'
import { ApplicationProvider, IconRegistry } from 'react-native-ui-kitten'
import { EvaIconsPack } from '@ui-kitten/eva-icons'

import TabNavigator from './src/navigation/TabNavigator'

const App = () => (
  <Fragment>
    <IconRegistry icons={EvaIconsPack} />
    <ApplicationProvider mapping={mapping} theme={lightTheme}>
      <TabNavigator />
    </ApplicationProvider>
  </Fragment>
)

export default App

If you want to use a third party icons library such as react-native-vector-icons, you can learn more on how to integrate that here with UI Kitten.

Open the TabNavigator.js file and import the Icon component from UI Kitten.

import { Icon } from 'react-native-ui-kitten'

Each route in the BottomTabNavigator has access to different properties via a navigationOptions object. To hide the label or the name of each screen and display an icon in place of, we return an Icon component on the tabBarIcon property inside navigationOptions.

When a specific route or the screen is focused, its icon color should appear darker than the other icons in the tab bar, just to indicate that it’s the active tab. This can be achieved using the prop focused on tabBarIcon.

Modify the tab navigator as follows:

const TabNavigator = createBottomTabNavigator(
  {
    Feed: {
      screen: Feed,
      navigationOptions: {
        tabBarIcon: ({ focused }) => (
          <Icon
            name='home-outline'
            width={32}
            height={32}
            fill={focused ? '#111' : '#939393'}
          />
        )
      }
    },
    AddPost: {
      screen: AddPost,
      navigationOptions: {
        tabBarIcon: ({ focused }) => (
          <Icon
            name='plus-square-outline'
            width={32}
            height={32}
            fill={focused ? '#111' : '#939393'}
          />
        )
      }
    }
  },
  {
    tabBarOptions: {
      showLabel: false
    }
  }
)

To display an Icon from UI Kitten, it’s required to provide width and height attributes.

The createBottomTabNavigator accepts the second parameter as a config object to modify the whole tab bar rather than each route. tabBarOptions is an object with different properties, such as hiding the label of each route by setting the boolean value of showLabel to false.

Here’s the output:

Mocking data in a feed screen

In this section, we’ll create a simple UI for the feed screen that contains the image and its title. Open the Feed.js file and import the following libraries:

import React, { Component } from 'react'
import { Image, View, TouchableOpacity, StyleSheet } from 'react-native'
import { Text, Layout, withStyles, List } from 'react-native-ui-kitten'

Let’s mock a DATA array with some static resources to display on the screen. Later, we’re going to fetch the data from Firestore.

const DATA = [
  {
    id: 1,
    postTitle: 'Planet of Nature',
    imageURI:
      'https://images.unsplash.com/photo-1482822683622-00effad5052e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80'
  },
  {
    id: 2,
    postTitle: 'Lamp post',
    imageURI:
      'https://images.unsplash.com/photo-1482822683622-00effad5052e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80'
  }
]

The List from UI Kitten extends the basic FlatList from react-native to render a list of items. It accepts the same amount of props as a normal flat list component. In a real application, having a FlatList is useful instead of ScrollView when there’s a large number of data items in the list to render to the user.

const Feed = () => {
  return (
    <Layout style={{ flex: 1 }}>
      <View
        style={{
          marginTop: 60,
          borderBottomWidth: StyleSheet.hairlineWidth,
          alignItems: 'center'
        }}>
        <Text style={{ fontSize: 20 }}>All Posts 🔥</Text>
      </View>
      <List
        style={this.props.themedStyle.container}
        data={DATA}
        renderItem={renderItem}
        keyExtractor={DATA.id}
      />
    </Layout>
  )
}

We’ll come back to the style attribute in the next section. The data attribute accepts the value of a plain array, hence the mock DATA array. Using keyExtractor allows the list to extract a unique key for each item in the list that’s rendered. The renderItem attribute accepts what to display in the list, or how to render the data.

UI Kitten has a default ListItem component that can be used to display individual items. But to customize, let us create our own. Add the following inside the render method of the component but before the return statement:

const renderItem = ({ item }) => (
  <View style={this.props.themedStyle.card}>
    <Image
      source={{ uri: item.imageURI }}
      style={this.props.themedStyle.cardImage}
    />
    <View style={this.props.themedStyle.cardHeader}>
      <Text category='s1' style={this.props.themedStyle.cardTitle}>
        {item.postTitle}
      </Text>
    </View>
  </View>
)

Using high order functions to add styles

UI Kitten provides a theme-based design system that you can customize to your needs in the form of a JSON object. These theme variables help us create custom themes based on some initial values and support React Native style properties at the same time.

We have already imported withStyles HOC from UI Kitten. It accepts a component that can use the theme variables. In our case, the Feed component.

First, to identify the class component it accepts and the one it returns, edit the following line:

class _Feed extends Component {
  // ...
}

Add the following style when exporting the Feed component. These styles are in the prop style.

export default Feed = withStyles(_Feed, theme => ({
  container: {
    flex: 1
  },
  card: {
    backgroundColor: theme['color-basic-100'],
    marginBottom: 25
  },
  cardImage: {
    width: '100%',
    height: 300
  },
  cardHeader: {
    padding: 10,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between'
  },
  cardTitle: {
    color: theme['color-basic-1000']
  }
}))

Here’s the output:

Add Firebase queries

Before proceeding with this section, please make sure you have successfully followed instructions to install and integrate the react-native-firebase library in your React Native app. Also, make sure that you’ve set up a Firebase app and have the right to access Firestore.

Create a utils/ directory in src and add a new file named Firebase.js. This file will contain two methods that will handle uploading an image with relevant post data to Firestore in a collection called posts.

The uuid package helps to create a unique identifier for each post to be uploaded.

import firebase from 'react-native-firebase'
import uuid from 'uuid'

const Firebase = {
  //...
}

export default Firebase

The first method is to upload the image and its title.

uploadPost: post => {
 const id = uuid.v4()
 const uploadData = {
 id: id,
 postPhoto: post.photo,
 postTitle: post.title
 }
 return firebase
 .firestore()
 .collection('posts')
 .doc(id)
 .set(uploadData)
 },

The second method is used to fetch all the posts from the collection posts. The Feed component requires the Firebase query to fetch all posts with one request. Using querySnapshot.docs.map, you can fetch multiple documents at once from the Firestore database. Add the below snippet:

getPosts: () => {
  return firebase
    .firestore()
    .collection('posts')
    .get()
    .then(function(querySnapshot) {
      let posts = querySnapshot.docs.map(doc => doc.data())
      // console.log(posts)
      return posts
    })
    .catch(function(error) {
      console.log('Error getting documents: ', error)
    })
}

Using the Context API

Using the Context API, you can easily consume Firebase methods in the app without adding a state management library like Redux.

The most common reason to use the Context API in a React Native app is that you need to share some data in different places or components in the component tree. Manually passing props can be tedious as well as hard to keep track of.

Create a new file called utils/FirebaseContext.js. It will hold the snippet for creating the context and a high order function. The HoC will eliminate the need for importing and using Firebase.

Wrapping each component as a parameter to the HoC will provide access to Firebase queries (or the custom methods created in file Firebase.js) as props.

import React, { createContext } from 'react'

const FirebaseContext = createContext({})

export const FirebaseProvider = FirebaseContext.Provider

export const FirebaseConsumer = FirebaseContext.Consumer

export const withFirebaseHOC = Component => props => (
  <FirebaseConsumer>
    {state => <Component {...props} firebase={state} />}
  </FirebaseConsumer>
)

Create a new file utils/index.js to export both the Firebase object from the Firebase.js file, the provider, and the HoC.

import Firebase from './Firebase'
import { FirebaseProvider, withFirebaseHOC } from './FirebaseContext'

export default Firebase

export { FirebaseProvider, withFirebaseHOC }

The provider has to grab the value from the context object for the consumer to use that value. This is done in the App.js file. The value for the FirebaseProvider is going to be the Firebase object.

import React, { Fragment } from 'react'
import { mapping, light as lightTheme } from '@eva-design/eva'
import { ApplicationProvider, IconRegistry } from 'react-native-ui-kitten'
import { EvaIconsPack } from '@ui-kitten/eva-icons'

import Firebase, { FirebaseProvider } from './src/utils'
import TabNavigator from './src/navigation/TabNavigator'

const App = () => (
  <Fragment>
    <IconRegistry icons={EvaIconsPack} />
    <ApplicationProvider mapping={mapping} theme={lightTheme}>
      <FirebaseProvider value={Firebase}>
        <TabNavigator />
      </FirebaseProvider>
    </ApplicationProvider>
  </Fragment>
)

export default App

Upload images to Firestore

The AddPost component will allow a user to choose an image from their phone’s gallery and upload it to the Firestore database. Open the AddPost.js file and add the following import statements:

import React, { Component } from 'react'
import { Image, View } from 'react-native'
import { Text, Button, Input } from 'react-native-ui-kitten'
import ImagePicker from 'react-native-image-picker'
import { withFirebaseHOC } from '../utils'

Add a state object to keep track when an image file is picked from the gallery, as well as when there’s a title provided for that image file. This is to be done inside the class component.

class AddPost extends Component {
  state = {
    image: null,
    title: ''
  }
  onChangeTitle = title => {
    this.setState({ title })
  }

  // ...
}

An image can be picked using ImagePicker.launchImageLibrary() from react-native-image-picker. Do note that this method expects an options object as the parameter. If an image is picked successfully, it will provide the URI of the image.

selectImage = () => {
  const options = {
    noData: true
  }
  ImagePicker.launchImageLibrary(options, response => {
    if (response.didCancel) {
      console.log('User cancelled image picker')
    } else if (response.error) {
      console.log('ImagePicker Error: ', response.error)
    } else if (response.customButton) {
      console.log('User tapped custom button: ', response.customButton)
    } else {
      const source = { uri: response.uri }
      console.log(source)
      this.setState({
        image: source
      })
    }
  })
}

The onSubmit asynchronous method is responsible for uploading the post to the Firestore and clearing the state object when the post is successfully uploaded.

onSubmit = async () => {
  try {
    const post = {
      photo: this.state.image,
      title: this.state.title
    }
    this.props.firebase.uploadPost(post)

    this.setState({
      image: null,
      title: '',
      description: ''
    })
  } catch (e) {
    console.error(e)
  }
}

Here is the snippet of the render function:

render() {
 return (
 <View style={{ flex: 1, marginTop: 60 }}>
 <View>
 {this.state.image ? (
 <Image
 source={this.state.image}
 style={{ width: '100%', height: 300 }}
 />
 ) : (
 <Button
 onPress={this.selectImage}
 style={{
 alignItems: 'center',
 padding: 10,
 margin: 30
 }}>
 Add an image
 </Button>
 )}
 </View>
 <View style={{ marginTop: 80, alignItems: 'center' }}>
 <Text category='h4'>Post Details</Text>
 <Input
 placeholder='Enter title of the post'
 style={{ margin: 20 }}
 value={this.state.title}
 onChangeText={title => this.onChangeTitle(title)}
 />
 <Button status='success' onPress={this.onSubmit}>
 Add post
 </Button>
 </View>
 </View>
 )
 }
}

export default withFirebaseHOC(AddPost)

This is the output you should see for the AddPost screen:

Click on the button Add an image and choose the image from the device’s gallery or stored images:

By clicking the button Add post, the post will be submitted to Firestore, which you can verify by opening the Firebase console. You will find a posts collection in your Firebase project.

Fetching posts from Firestore

Open Feed.js and import the following statements:

import { withFirebaseHOC } from '../utils'

Create a state object with two properties in the class component. You won’t need to mock a DATA array anymore.

state = { DATA: null, isRefreshing: false }

Create a handler method called fetchPosts to fetch the data. You have to explicitly call this method in the lifecycle method componentDidMount to load all posts available, since Feed is the entry screen:

componentDidMount() {
 this.fetchPosts()
 }

 fetchPosts = async () => {
 try {
 const posts = await this.props.firebase.getPosts()
 console.log(posts)
 this.setState({ DATA: posts, isRefreshing: false })
 } catch (e) {
 console.error(e)
 }
 }

Next, add another method called onRefresh that’s responsible for fetching posts when the screen is pulled downwards.

onRefresh = () => {
  this.setState({ isRefreshing: true })
  this.fetchPosts()
}

Here’s what the rest of the component will look like. While the data is being currently fetched, it will show a loading indicator on the screen:

render() {
 const renderItem = ({ item }) => (
 <View style={this.props.themedStyle.card}>
 <Image
 source={{ uri: item.postPhoto.uri }}
 style={this.props.themedStyle.cardImage}
 />
 <View style={this.props.themedStyle.cardHeader}>
 <Text category='s1' style={this.props.themedStyle.cardTitle}>
 {item.postTitle}
 </Text>
 </View>
 </View>
 )
 if (this.state.DATA != null) {
 return (
 <Layout style={{ flex: 1 }}>
 <View
 style={{
 marginTop: 60,
 borderBottomWidth: StyleSheet.hairlineWidth,
 alignItems: 'center'
 }}>
 <Text style={{ fontSize: 20 }}>All Posts 🔥</Text>
 </View>
 <List
 style={this.props.themedStyle.container}
 data={this.state.DATA}
 renderItem={renderItem}
 keyExtractor={this.state.DATA.id}
 refreshing={this.state.isRefreshing}
 onRefresh={() => this.onRefresh()}
 />
 </Layout>
 )
 } else {
 return (
 <View
 style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
 <ActivityIndicator size='large' />
 </View>
 )
 }
 }

Also, don’t forget to add withFirebaseHOC:

export default Feed = withFirebaseHOC(
 withStyles(_Feed, theme => ({
 //.. remains same
)

On the initial load, since there’s only one post in the posts collection, the output will be the following:

Conclusion

There are many useful strategies for using Firebase and React Native together that you can take from this post. Also, using a UI library like react-native-ui-kitten saves a lot of time in figuring out how to style each component.

You can find the complete source code at this GitHub repo.

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *