Building a React/Firestore app with zero effort and mobx 🔥

Hein Rutjes
5 min readDec 31, 2017

--

Firestore is an amazing new NoSQL database with real-time updating capabilities. Like the original Firebase real-time database, but designed from the ground up to use the Google cloud infrastructure and be infinitely scalable.

Firestore has a great API, but integrating it efficiently into React isn’t always straightforward. Let’s take a look at a basic TodoList example:

import firebase from 'firebase';class TodoList extends React.Component {
constructor() {
this.colRef = firebase.firestore().collection('todos');
this.state = {
fetching: false,
docs: []
};
}
componentDidMount() {
this.unsubscribeCol = this.colRef.onSnapshot(this.onColUpdate);
this.setState({fetching: true});
}
componentWillUnmount() {
this.unsubscribeCol();
}
render() {
const {docs, fetching} = this.state;
return <div>
{docs.map((doc) => <TodoItem key={doc.id} doc={doc} />)}
</div>;
}
onColUpdate = (snapshot) => {
const docs = snapshot.docs.map((docSnapshot) => ({
id: docSnapshot.id,
data: docSnapshot.data()
}));
this.setState({
docs: docs,
fetching: false
});
};
}
class TodoItem extends React.Component {
render() {
const {doc} = this.props;
const {finished, text} = doc.data;
return <div>
<input type='check' checked={finished} />
<input type='text' value={text || ''} />
</div>;
}
}

Pretty cool, so this would listen for changes on the firestore collection and render it, not a bad start. However, this is exactly what it is, just a start. It isn’t particularly performant either at this point. Whenever anything changes in the collection, TodoList is re-rendered and all TodoItem components are also re-rendered. And at the same time, we probably still want to add sub-queries, editing of items, adding items, deleting, etc…

When you’re building an Application for scale, you’ll likely have many firestore driven views, so this will add a lot of boilerplate code to your application.

Introducing Firestorter

firestorter attempts to solve these problems by providing an simple to use API on top of a super performant MobX observable architecture. Let’s take a look on how to build the same TodoList with Firestorter:

import {Collection} from 'firestorter';
import {observer} from 'mobx-react';
const TodoList = observer(class TodoList extends React.Component {
constructor() {
this.col = new Collection('todo');
}
render() {
const {docs, fetching} = this.col;
return <div>
{docs.map((doc) => <TodoItem key={doc.id} doc={doc} />)}
</div>;
}
});
// Optionally, you can also use the @decorator syntax, but you
// might need to install some plugins for that
@observer
class TodoItem extends React.Component {
render() {
const {doc} = this.props;
const {finished, text} = doc.data;
return <div>
<input type='check' checked={finished} />
<input type='text' value={text || ''} />
</div>;
}
};

That’s it really. TodoList will now automatically subscribe for firestore snapshot updates when it is rendered, and automatically unsubscribe when data is no longer rendered; or the component is unmounted.

But that’s not all. Updating of the components is now as efficient as can be. When for instance only one of the Todo documents changes, only that particular TodoItem component is re-rendered. In fact, updating is so efficient that TodoList would not be re-rendered in that case. TodoList will only re-render when documents are added, removed or moved.

The power of MobX and observables

At the heart of Firestorter is an architecture which leverages MobX’s reactive observables. Using the observer pattern, Firestorter can determine whether a Collection or Document is actually being rendered or not. When this is not the case, real-time updating (snapshot updates) is stopped automatically, causing only the data that is in view to live fetched. You can find more information on MobX observables and the observer pattern here:

Adding documents

Adding documents can be best done using Collection.add, which automatically assigns it a document ID:

import {Collection} from 'firestorter';const todos = new Collection('todos');
const doc = await todos.add({
finished: false,
text: 'new task'
});
console.log(doc.id);

Collection.add() returns a Promise with the newly created document. If for whatever reason the operation failed (e.g. no permissions to add the document), the promise is rejected with the appropriate error.

Alternatively, you can use the Document interface to create documents with custom IDs:

import {Document} from 'firestorter';const todo = new Document('todos/mydoc');// Use .set to create the document in the back-end
todo.set({
finished: false,
text: 'this is awesome'
});

Updating documents

Updating documents can be done in 3 different ways:

  • Update one or more fields
  • Replace document contents
  • Merge data into document
const todo = new Document('todos/akskladlkasd887asj');// Update one or more fields
await doc.update({
finished: true,
settings: {}
});
// Update a nested property using a field-path
await doc.update({
'settings.foo.bar': 56
});
// Replace document content using .set
await doc.set({
blank: true
});
// Merge data into the document
await doc.set({
settings: {
foo2: 'hello'
}
}, {merge: true});

Deleting documents

To delete a document, use Document.delete:

const todo = new Document('todos/akskladlkasd887asj');// Delete the document
await doc.delete();

Queries

Using queries, you can filter, sort and limit the data that is returned in a collection. By default, when you create a Collection, it will fetch all the data in that collection. Use the query property to set a Firestore Query on the Collection.

const todos = new Collection('todos');// Show only the documents that not finished, with a max-limit of 10
todos.query = todos.ref.where('finished', '==', false).limit(10);
...// By resetting the query, the Collection reverts to fetching
// all the documents in the collection
todos.query = undefined;

Take a look at the Firestorter TodoApp, to see this query in action: react-firestore-todo-app?file=Todos.js

Final takeaways

This concludes the introduction to Firestorter. As you can see, you don’t need to write much code to hook up your React app to Google Firestore. And this is exactly the purpose of Firestorter, to provide a simple, clean and elegant API that takes a way the burden of managing snapshot updates, data caching and efficiently updating your components.

However, this is not where Firestorter stops. Firestorter intents to become your one stop solution for everything React/Firestore related. This introduction only scratched the basic API, but there is much more to be discovered in the firestorter github repository. Stay tuned for the next article, which will cover:

  • Document schemas (using superstruct)
  • Manual fetching and the fetching property
  • Indirect (reactive) collection/document paths
  • Nested collections
  • Deleting collections
  • And more

Resources

Firestorter

--

--

Hein Rutjes

Tech creator, App developer, React-native & JavaScript enthusiast. Likes animations & Firebase, @expo