Getting started with Cloud Firestore on React Native
A week ago, Firebase announced Cloud Firestore, an awesome NoSQL document database that complements the existing Realtime Database. React Native Firebase (RNFirebase) is proud to announce support for both Android & iOS on React Native.
To get started, we’ve made a starter app which is all setup and ready to go — simply clone/download it and follow the instructions in the README!
Building a TODO app with Cloud Firestore & RNFirebase
Lets go ahead and build a simple TODO app with Cloud Firestore & RNFirebase. Assuming you’ve used the starter app mentioned above or added react-native-firebase
manually to an existing project (the docs are here), we can get started!
Create a new file Todos.js
in the root of your React Native project and point your index.android.js
/ index.ios.js
files to load it:
import { AppRegistry } from 'react-native';
import Todos from './Todos';AppRegistry.registerComponent('RNFirebaseStarter', () => Todos);
Next setup a basic React class in Todos.js
:
import React from 'react';class Todos extends React.Component {
render() {
return null;
}
}export default Todos;
Creating your Cloud Firestore data structure
Cloud Firestore allows documents (objects of data) to be stored in collections (containers for your documents). Our TODO app will simply hold a list of todo documents within a single “todos” collection. Each document contains data specific to that todo — in our case title
and complete
properties.
The first step is to create a reference to the collection, which can be used throughout our component to query it. We’ll import react-native-firebase
and create this reference in our component constructor.
import React from 'react';
import firebase from 'react-native-firebase';class Todos extends React.Component {
constructor() {
super();
this.ref = firebase.firestore().collection('todos');
}
...
Building the UI
For blog readability, we’ll add the styles in-line however you should use StyleSheet for production apps.
The screenshots are from an Android emulator, however the following will also work in an iOS environment!
The UI will be simple: a scrollable list of todos, along with a text input to add new ones. Lets go ahead and build out our render
method:
import { ScrollView, View, Text, TextInput, Button } from 'react-native';
...render() {
return (
<View>
<ScrollView>
<Text>List of TODOs</Text>
</ScrollView>
<TextInput
placeholder={'Add TODO'}
/>
<Button
title={'Add TODO'}
disabled={true}
onPress={() => {}}
/>
</View>
);
}
You should now see a dummy scrollview, a text input and a button which does nothing… something similar to the following:
We now need to hook the text input up to our local state so we can send the value to Cloud Firestore when the button is pressed.
Add the constructor
default state:
constructor() {
super();
this.ref = firebase.firestore().collection('todos');
this.state = {
textInput: '',
};
}
Add the method updateTextInput
to update component state when the TextInput
value updates:
updateTextInput(value) {
this.setState({ textInput: value });
}
Hook up the TextInput
to state and trigger a state update on text change:
<TextInput
placeholder={'Add TODO'}
value={this.state.textInput}
onChangeText={(text) => this.updateTextInput(text)}
/>
Change the buttons disabled state based on the length of the text input value:
<Button
title={'Add TODO'}
disabled={!this.state.textInput.length}
onPress={() => {}}
/>
Your app should now respond to text changes, with the value reflecting local state and your buttons disabled state:
Adding a new Document
To add a new document to the collection, we can call the add
method on the collection reference. Lets hook our button up and add the todo!
Add a addTodo
method:
addTodo() {
this.ref.add({
title: this.state.textInput,
complete: false,
}); this.setState({
textInput: '',
});
}
Let the button trigger the method:
<Button
title={'Add TODO'}
disabled={!this.state.textInput.length}
onPress={() => this.addTodo()}
/>
When our button is pressed, the new todo is sent to Cloud Firestore and added to the collection. We then reset the textInput component state. The add
method is asynchronous and returns the DocumentReference from a Promise if required.
Make sure you have permission to add data to the collection on your Rules page!
Subscribing to collection updates
Even though we’re populating the collection, we still need to display the documents on our app. Cloud Firestore provides two methods; get()
queries the collection once and onSnapshot()
which gives updates in realtime when a document changes. For our TODO app, we’ll want realtime results. Lets go ahead and setup some more component state to handle the data.
Add a loading
state and todos
state to the component:
constructor() {
super();
this.ref = firebase.firestore().collection('todos');
this.unsubscribe = null; this.state = {
textInput: '',
loading: true,
todos: [],
};
}
We need a loading
state to indicate to the user that the first connection to Cloud Firestore hasn’t yet completed. We also added a unsubscribe
class property which we’ll see the usage of next.
Subscribe to collection updates on component mount:
componentDidMount() {
this.unsubscribe = this.ref.onSnapshot(this.onCollectionUpdate)
}
componentWillUnmount() {
this.unsubscribe();
}
onSnapshot
returns an “unsubscriber” function to allow us to stop receiving updates, which we call when the component is about to unmount.
Implement the onCollectionUpdate
method:
onCollectionUpdate = (querySnapshot) => {}
Note: an arrow function (=>
) is used here to ensure the onCollectionUpdate
method is bound to the Todos component scope.
Iterate over the documents and populate state:
onCollectionUpdate = (querySnapshot) => {
const todos = []; querySnapshot.forEach((doc) => {
const { title, complete } = doc.data(); todos.push({
key: doc.id,
doc, // DocumentSnapshot
title,
complete,
});
}); this.setState({
todos,
loading: false,
});
}
We use the snapshot forEach
method to iterate over each DocumentSnapshot
in the order they are stored on Cloud Firestore, and grab the documents unique ID (.id
) and data (.data()
). We also store the DocumentSnapshot
in state to access it directly later.
Every time a document is created, deleted or modified on the collection, this method will trigger and update component state.
Rendering the todos
Now we have the todos loading into state, we need to render them. A ScrollView
is not practical here as a list of TODOs with many items will cause performance issues when updating. Instead we’ll use a FlatList.
Handle loading
state:
render() {
if (this.state.loading) {
return null; // or render a loading icon
}
Render the todos in a FlatList
using the todos
state:
import { FlatList, Button, View, Text, TextInput } from 'react-native';import Todo from './Todo'; // we'll create this next
...render() {
if (this.state.loading) {
return null; // or render a loading icon
} return (
<View style={{ flex: 1 }}>
<FlatList
data={this.state.todos}
renderItem={({ item }) => <Todo {...item} />}
/>
<TextInput
placeholder={'Add TODO'}
value={this.state.textInput}
onChangeText={(text) => this.updateTextInput(text)}
/>
<Button
title={'Add TODO'}
disabled={!this.state.textInput.length}
onPress={() => this.addTodo()}
/>
</View>
);
}
You may notice that we’ve got a Todo
component rendering for each item. Below we’ll quickly create this as a PureComponent
. This will provide huge performance boosts in our app as each row will only re-render when a prop (title
or complete
) changes.
Create a Todo.js
file in the root of your project:
import React from 'react';
import { TouchableHighlight, View, Text } from 'react-native';export default class Todo extends React.PureComponent { // toggle a todo as completed or not via update()
toggleComplete() {
this.props.doc.ref.update({
complete: !this.props.complete,
});
}
render() {
return (
<TouchableHighlight
onPress={() => this.toggleComplete()}
>
<View style={{ flex: 1, height: 48, flexDirection: 'row', alignItems: 'center' }}>
<View style={{ flex: 8 }}>
<Text>{this.props.title}</Text>
</View>
<View style={{ flex: 2 }}>
{this.props.complete && (
<Text>COMPLETE</Text>
)}
</View>
</View>
</TouchableHighlight>
);
}
}
This component just renders out the title and whether the todo document is completed or not. It’s wrapped in a TouchableHighlight
component, allowing us to make it a touchable row. When the row is pressed, we can grab the ref (DocumentReference
) directly from the doc
prop (DocumentSnapshot
) and update it using the update
method (which again is asynchronous if you wish to handle errors).
We can now toggle the todos completed state and not worry about local state as our onSnapshot
subscription propagates the updates from Cloud Firestore back to our component in real time, awesome huh?!
You should see the following — realtime interaction with Cloud Firestore!
This is just a taster of what react-native-firebase
and Cloud Firestore can do for your React Native applications. If you want to keep up to date, you can follow us here, on Twitter, over at the main GitHub repo or chat with us on Discord.
Thanks for reading! If you liked this story, please click the 👏 below so other people will see it on Medium (it really helps, otherwise it doesn’t get into others’ feeds). Sharing on Facebook and re-tweeting is appreciated too!