« Back to blog posts

Getting started with Cloud Firestore on React Native

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 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) => {
  // TODO
}

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.


Share this blog post: