This site runs best with JavaScript enabled.

Upload images in React Native apps using Firebase and Firestore

Aman Mittal

November 21, 2019

Fullstack Developer

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
  • Use Context API
  • Upload images to Firestore
  • Fetching posts from Firestore
  • Conclusion

Stack/Requirements

Setting up UI Kitten

Before you'd want to dwell 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 they are presented below.

react-native init rnFirestoreUploadExample
# after the project directory is created
cd rnFirestoreUploadExample
# install the following
yarn add react-navigation react-native-svg react-native-screens@1.0.0-alpha.23 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 am 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.

UI kitten library provides a default light and dark theme that could 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 will 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

You will get the following output at this stage in an emulator.

ss1

I am not going to dwell on setting up react-navigation library. 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 them all the posts uploaded to the Firestore and the second screen is to allow us to upload the post. Each post will contain the image and its title.

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

Open Feed.js file and 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

Open 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 TabNavigator.js inside 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 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 screen component active in blue color by default. Here is the output:

ss2

Adding icons to tab bar

Instead of displaying names for each screen, display the appropriate icons. You have already installed the icon library. Modify App.js file to integrate icons from @ui-kitten/eva-icons that 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 third party icons library such as react-native-vector-icons you can learn more here on how to integrate that here with UI kitten.

Open 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 navigationOptions object. To hide the label or the name of each screen and display an icon in place of it is achieved by returning an Icon component on tabBarIcon property inside navigationOptions.

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

Modify the tab navigator as the following:

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 the UI Kitten, it is required to provide width and height attributes.

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

Here is the output:

ss3

Mocking data in a Feed Screen

In this section, create a simple UI for the Feed screen that contains the image and its title. Open 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, you are going to fetch the data from the Firestore when the

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 React Native 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 is 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 will 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 gives the List to extract a unique key for each item in the list that is 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 themed base design system that you can customize to your needs in the form of a JSON object. These theme variables help to 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, just 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 is the output:

ss4

Add Firebase queries

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

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

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

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)
})
}

Use Context API

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

The common reason to use 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. The consumer in every component necessary. By 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 going to be 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 on 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 is 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 })
}
// ...
}

Using ImagePicker.launchImageLibrary() from react-native-image-picker an image can be picked. 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 to upload the post to the Firestore and clear 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 get for AddPost screen.

ss5

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

ss6

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.

ss7

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 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 is responsible to fetch posts when the screen is pulled downwards.

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

Here is how 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, do not forget to add withFirebaseHOC.

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

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

ss8

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 to figure out how to style each component.

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

Originally published at Heartbeat.Fritz.AI

Share article

Aman Mittal

I'm Aman a Computer Science engineer working as an independent fullstack developer. I specialize in web & mobile platforms working with technologies such as Node.js, ReactJS, and React Native. I write tutorials for JavaScript Web and Mobile developers.

Support at ko-fi.com