The simplest way to combine React, Redux and Firestore (Typescript)

Naum Shapkarovski
Exelerate Blog
Published in
5 min readMay 18, 2021

--

The useFirestore hook!

In this article I will show you the simplest way to combine React, Redux and Firestore — template Typescript :)

We will create a generic useFirestore() hook so that you can get data from Firestore with a few lines of code, as well as implement lazy load and listen to changes.

At Exelerate, we first tried using different packages for this scenario, but nothing seemed to suit our needs. So, we decided to do something basic but scalable and adapt this method to our scenario. This article will explain and dissect our approach, so you have the freedom to modify the code, customize it and apply it to your situation.

This article requires prior knowledge of the technologies mentioned above because it is intended for anyone who has trouble combining them, while being easy to use with clean code. If you are new to any of these technologies, read the official documentation first.

Start with creating an app and adding Redux!

To make this easy to understand we will create a simple React app and Firebase project called “the-usefirestore-hook”.

To combine React, Redux, and Typescript without going through a full setup with Redux, we will use the powerful Redux Toolkit, created by the Redux team allowing us to avoid the setup.

It was originally created to help address three common concerns about Redux:

  • “Configuring a Redux store is too complicated”
  • “I have to add a lot of packages to get Redux to do anything useful”
  • “Redux requires too much boilerplate code”

Since we are creating a new app, the recommended way to start new apps with React, Redux Toolkit and Typescript is by using the official Redux+TS template for Create React App, which takes advantage of React Redux’s integration with React components.

npx create-react-app the-usefirestore-hook --template redux-typescript

If you want to add Redux Toolkit to an existing app, just

yarn add @reduxjs/toolkit

If you are not familiar with Redux Toolkit, check the official doc here, or you can adapt the hook with your Redux setup i.e., use your actions because it is dependent only on reducer actions.

Adding Firebase/Firestore

First, we need to install and then configure it.

yarn add firebase

Store your firebase app config in a .env file (you can find it in the firebase console). Create the config.ts file inside the app directory and export an object with the firebase env variables.

Now, let’s configure our firebase app in the firebase.ts file, inside the firebase-config directory.

Storing mock data in Firestore

I will create two collections (notifications, users) and add some documents so we can test the app. At the bottom of this article, you can find my GitHub repo that contains mock data and the whole code we will create.

Because we use Typescript, I want to create models for the collections in my models directory:

And the User model

Creating Generic Slice

slices/generic.ts

First, we will create an interface for our GenericState.

The field data will be the data returned from firestore (in our case notifications or users). Status is an “enum” that has “Loading”, “Error” or “Done”. The “errors” field is for handling errors.

Now, we can create a genericSlice function with the GenericState model.

The slice is generic because it takes an initialState with a generic type T of the data as a parameter.

The slice also takes a name and reducers as parameters and returns a slice with three reducers (loading, success, error) plus the new reducers that we will pass in as arguments when creating the actual slice (ex. notificationSlice).

Now, we can create our notification slice for the notifications that we read from our database.

Create a new file notification.ts in the slice directory:

If you noticed in the code, we override the success reducer because we want to map the data before it is dispatched, i.e., in the database the “createdAt” field is of type Timestamp, and we want to convert it to a string, then store it in our store.

Redux Toolkit enables this with the prepare function.

If there is no mapping, the reducers object would be empty.

At the bottom we export the actions (success, loading, error) that we pass in as arguments in the hook we make.

Also, we export the reducer and add it to the root in the store.ts file in configureStore:

We can jump to the main part of this article now, creating the useFirestore hook!

First of all we will create custom types for collection and document options that we will pass in when getting or streaming data.

In the firebase-config/queryOptions.ts file add this:

Those options will be available when we use the useFirestore hook.

We will add two files more, getQuery.ts and getFirestore.ts.

The getQuery.ts file contains a getQuery() method that returns a query based on collection options that we pass in as an argument.

The getFirestore.ts file contains a getFirestoreRef() method that returns a reference from the firestore collection that we use. We use mode/development as a basic path.

Let’s create the hook!

In the hooks/useFirestore.ts:

The useFirestore hook takes only a collection path as a parameter and a generic type T, which is a model for each document in that collection. In our specific case this is Notification or User.

We keep a reference of all listeners in the collection and the documents to which we subscribe. A case where we can have more listeners for one collection is lazy load or infinite scroll, i.e., on each load a new listener is activated which we listen to.
We can also have multiple listeners for different documents from the same collection.

The snapshot Firestore listener returns an unsubscriber, so in the useEffect destructor we unsubscribe from all listeners, i.e. we unsubscribe as soon as the component in which we use the useFirestore hook is unmounted.

The collection() method takes an object of type CollectionOptios as a parameter (we created above) and generic actions of type GenericActions that we will create in the slices/generic.ts file.

The doc() method takes an id of a document, actions and options of type DocumentOptions.

The logic is executed in two methods, collectionApi() and docApi(). We take the data from the database and send it to the global store through the actions we created earlier, and we pass them in as arguments when calling the collection function.

We are done with the logic for getting data.

The next methods are for creating, updating and deleting a document.

The last method is unsubscribe(). It takes listenerName as an optional parameter and unsubscribes of that specific listener or unsubscribes of all listeners if there is no listenerName. This method is useful if we want to unsubscribe from the component manually.

Using the useFirestore hook!

It’s that simple!

Be careful to invoke the collection method on a component mount or on some event like button click, and not outside because it will enter a loop.

You can call firestore.unsubscribe() to unsubscribe off all listeners or firestore.unsubscribe(‘listenerName’) to unsubscribe from a specific listener.

All the code above is available on my GitHub account. You can clone the repo and see the magic of combining React, Redux and Firestore using Typescript. Also, there is a lazy load example in the code.

Once you clone it, insert your firebase config in the .env file, run “yarn && yarn start”, and click the button “Add Mock Data” on the home page. Then you can test the hook we just created.

Thanks for reading!

--

--

Naum Shapkarovski
Exelerate Blog

Co-founder & CTO @ Exelerate · Focused on building startups and helping entrepreneurs