This blog post has been updated for version >=6.0.0 of React Native Firebase.
Late last year, Firebase announced Cloud Firestore, a NoSQL document database that compliments the existing Realtime Database product. React Native Firebase has since provided support for using it on both Android & iOS in React Native, right from day one of the release.
Building a TODO app with Cloud Firestore & React Native
Let’s go ahead and start building a TODO app with Cloud Firestore & React Native Firebase.
Assuming you’ve integrated React Native Firebase and added @react-native-firebase/app
& @react-native-firebase/firestore
to your project (using these docs), 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 it:
import { AppRegistry } from 'react-native';
import Todos from './Todos';
AppRegistry.registerComponent('MyAppName', () => Todos);
Next; set up a basic React component in Todos.js
:
import React from 'react';
function Todos() {
return null;
}
export default Todos;
Create your Cloud Firestore data structure
Cloud Firestore allows documents (objects of data) to be stored in collections (think of them as containers for your documents). Our TODO app will hold a list of todo documents within a single “todos” collection only for simplicity. Each document contains the data specific to that todo – in our case the 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/firestore
and create this reference in our component:
import React from 'react';
import firestore from '@react-native-firebase/firestore';
function Todos() {
const ref = firestore().collection('todos');
return null;
}
Create a user interface
For simplicity, we’ll use react-native-paper for our UI – a great library for React Native which provides pre-built React components that follow Googles Material Design guidelines. It’s super easy to install, head over to their documentation on how to get started.
Let’s now create a simple UI with a scrollable list of todos, along with a text input to add new ones:
import React from 'react';
import { ScrollView, Text } from 'react-native';
import firestore from '@react-native-firebase/firestore';
import { Appbar, TextInput, Button } from 'react-native-paper';
function Todos() {
const ref = firestore().collection('todos');
return (
<>
<Appbar>
<Appbar.Content title={'TODOs List'} />
</Appbar>
<ScrollView style={{flex: 1}}>
<Text>List of TODOs!</Text>
</ScrollView>
<TextInput label={'New Todo'} onChangeText={() => {}} />
<Button onPress={() => {}}>Add TODO</Button>
</>
);
}
You should now see the example scroll view, a text input, and a button which does nothing – something similar to the following:
We now need to connect the text input to our local state, so we can send the value to Cloud Firestore when the button is pressed; subsequently adding the new TODO item.
We’ll use the useState
hook here, and update state every time the text changes via the onChangeText
prop from the TextInput
component.
Modify our Todos component and add the new state item with an initial state of an empty string:
import React, { useState } from 'react';
function Todos() {
const [ todo, setTodo ] = useState('');
const ref = firestore().collection('todos');
// ...
}
Then set the state item to be the value
of our TextInput
and the onChangeText
to call out setTodo
function; which will update our state whenever the user enters text into the input:
function Todos() {
const [ todo, setTodo ] = useState('');
const ref = firestore().collection('todos');
return (
<>
{/* ... */}
<TextInput label={'New Todo'} value={todo} onChangeText={setTodo} />
{/* ... */}
</>
);
}
Your app should now respond to text changes, with the value reflecting local state:
Adding new TODOs
To add a new document to the collection, we can call the add
method on our collection reference.
Create a new function in our component called addTodo
. This method will use our existing ref
variable to add a new item to the Firestore database.
function Todos() {
const [ todo, setTodo ] = useState('');
const ref = firestore().collection('todos');
// ...
async function addTodo() {
await ref.add({
title: todo,
complete: false,
});
setTodo('');
}
// ...
}
Update our button onPress
to call this new addTodo
method:
function Todos() {
// ...
return (
<>
{/* ... */}
<Button onPress={() => addTodo()}>Add TODO</Button>
</>
);
}
When the button is pressed, the new todo is sent to Cloud Firestore and added to the collection. We then reset the todo
state variable to clear the TextInput
box value.
The add()
method on the CollectionReference
is asynchronous and additionally returns the DocumentReference
for the newly created document.
If we check our collection on the Firebase Console we should now see our todo records being added to the collection:
If you’re having problems adding new documents; make sure your Cloud Firestore Security Rules allow writing to todos collection.
Subscribe to collection updates
Even though we’re populating the collection, we still want to display the documents on our app.
For reading documents; Cloud Firestore provides two ways;
-
get()
queries the collection once onSnapshot()
allows subscribing to updates to the query results (e.g. when a document changes) in realtime
As we want to subscribe to updates we’ll want to use onSnapshot()
so let’s go ahead and setup some additional component state to handle the updates and the subscription.
Add a loading
and todos
state to the component. The loading
state will default to true
, and the todos
state will be an empty array:
function Todos() {
const [ todo, setTodo ] = useState('');
const [ loading, setLoading ] = useState(true);
const [ todos, setTodos ] = useState([]);
// ...
}
We need a loading state to indicate to the user that the first connection (and initial data read) to Cloud Firestore has not yet completed.
With the useEffect
hook we can trigger a function to be called when the component first mounts. By returning the onSnapshot
function from useEffect
, the unsubscribe function that onSnapshot()
returns will be called when the component un-mounts.
import React, { useState, useEffect } from 'react';
// ...
function Todos() {
// ...
useEffect(() => {
return ref.onSnapshot((querySnapshot) => {
// TODO
});
}, []);
// ...
}
The query returns a QuerySnapshot
instance which contains the data from Firestore. We can Iterate over the documents and use it to populate state:
function Todos() {
// ...
const [ loading, setLoading ] = useState(true);
const [ todos, setTodos ] = useState([]);
// ...
useEffect(() => {
return ref.onSnapshot(querySnapshot => {
const list = [];
querySnapshot.forEach(doc => {
const { title, complete } = doc.data();
list.push({
id: doc.id,
title,
complete,
});
});
setTodos(list);
if (loading) {
setLoading(false);
}
});
}, []);
// ...
}
We use the snapshot forEach
method to iterate over each DocumentSnapshot
in the order they are stored on Cloud Firestore, and extract the documents unique identifier (.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 in realtime, neat!
We also check if loading
needs to be set back to false
. On the first load, this will disable loading – however after initial loading is complete we update the state in realtime so there is no need for the loading state again.
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 may cause performance issues when updating. Instead, we’ll use a FlatList
.
We’ll want to render differently if loading
state is true:
function Todos() {
// ...
if (loading) {
return null; // or a spinner
}
return (
// ...
);
}
When not loading, render the todos in a FlatList
using the todos
state we’re populating:
import { FlatList, Button, View, Text, TextInput } from 'react-native';
import Todo from './Todo'; // we'll create this next
// ...
function Todos() {
// ...
return (
<>
<Appbar>
<Appbar.Content title={'TODOs List'} />
</Appbar>
<FlatList
style={{flex: 1}}
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <Todo {...item} />}
/>
<TextInput label={'New Todo'} value={todo} onChangeText={setTodo} />
<Button onPress={() => addTodo()}>Add TODO</Button>
</>
);
}
You may notice that we’ve got a Todo
component rendering for each item. Let’s create this as a PureComponent
; this means each row will only need re-render if one of its props (title or complete) changes.
Create a Todo.js
file in the root of your project:
import React from 'react';
import firestore from '@react-native-firebase/firestore';
import { List } from 'react-native-paper';
function Todo({ id, title, complete }) {
async function toggleComplete() {
await firestore()
.collection('todos')
.doc(id)
.update({
complete: !complete,
});
}
return (
<List.Item
title={title}
onPress={() => toggleComplete()}
left={props => (
<List.Icon {...props} icon={complete ? 'check' : 'cancel'} />
)}
/>
);
}
export default React.memo(Todo);
This component renders out the title and whether the todo has been completed or not. Using react-native-paper
we return a List.Item
with an Icon on the left-hand side of the todo row/item. The icon changes based on the complete
status of the todo.
When the row is pressed, the toggleComplete
function is called; here we’ve set it to update the Firestore document with a reversed completion state.
Because our Todos
component is subscribed to the todos
collection, whenever an update
is made on an individual todo; the listener is called with our new data – which then filters down via state into our Todo
component.
Our final app should now look something like this:
At Invertase, we’re proud to be contributing to open-source and the Firebase community, and we hope you’ll love the new release of React Native Firebase.
Please get in touch via GitHub or Twitter if you have any issues or questions on this release.
With 💛 from the React Native Firebase team at Invertase.