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

preview of app we're building

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:

scroll view example

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:

state example

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:

firebase console showing firestore date

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:

screenshot of the final version of the app

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.

Share this post on social media

Stay Up To Date!

Newsleter Form

Stay updated with our latest news, blogs and software updates, subscribe to our newsletter.

By subscribing, you agree with Revue's Terms of Service and Privacy Policy.