« Back to blog posts

Getting started with Cloud Firestore on React Native

Getting started with Cloud Firestore on React Native

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.

To get started quickly; we recommend creating a new project, we’ve made a template just for this - follow the instructions in the documentation to get started. Once you have initialised your app from the template; you'll also need to npm/yarn install the @react-native-firebase/firestore package also.

Building a TODO app with Cloud Firestore & React Native

The app we'll be building in this blog post

Let's go ahead and start building a TODO app with Cloud Firestore & React Native Firebase.

Assuming you’ve used the template mentioned above or added @react-native-firebase/app & @react-native-firebase/firestore manually to an existing 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; setup 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 scrollview, a text input and a button which does nothing - something similar to the following:

Basic setup

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:

Text input in sync

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 inside of 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 the new todos in 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;

  1. get() queries the collection once
  2. 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) {
    reutrn null; // or a spinner
  }
  
  return (
    // ...
  );
}

And 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:

Firestore TODO app in React Native with realtime updates.

---------------

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, Twitter or Discord if you have any issues or questions on this release.

With 💛 from the React Native Firebase team at Invertase.


Share this blog post: