React and Firebase without Redux

Classic stock photo of “tools”

Even after spending a number of years contributing to tools to help integrate Firebase with Redux, such as react-redux-firebase and redux-firestore, I have recently been avoiding using Redux to store database data in new React + Firebase projects. In this article I’ll cover a bit about why, show what I have been doing instead, and finally how to easily start a new React + Firebase project using these tools.

Why?

Firebase SDK Manages State

Firebase’s client SDK manages caching and offline support internally. Having another copy of database state in redux is not only prone is inconsistencies, but also requires more code to manage that state.

Data Loading Is Async By Nature

Loading data from a database or service takes time. Writing logic to handle these loading conditions from data coming from redux state often times leads to if statements in component code which can quickly get unwieldy. Also, the logic used to pull this data out of redux state is often repeated in many places.

It would be great to have loading state available right in the component where we are attaching the listener. To simplify things even more, it would be great if we could leverage React’s Suspense to give us the syntactic sugar to declaratively specify our loading logic which makes it easier to reason about and debug. We can cover a bit more about why this can be a great pattern later, let’s get into how we can do this!

How?

We will use reactfire which includes hooks, context providers, and components to simplify interaction with Firebase. Setup includes using the FirebaseAppProvider, which is:

A React Context Provider that allows the useFirebaseApp hook to pick up the firebase object.

This means that once we setup this context provider we will be able to access the initialized Firebase app instance throughout our application.

Hooks are provided for accessing auth, subscribing to data, loading data once, and even for lazy loading parts of the Firebase SDK (requires Suspense mode to be enabled, will be covered in second example).

Here is an example of stitching these pieces together to load a list of projects from Firestore:

Whats Happening

  1. The top level of the App is setup with FirebaseAppProvider — this is a Context provider for storing the firebase SDK instance. Since this is a provider, children within it will be rendered and will have the ability to use the values of that context.
  2. In the Projects component the useFirestore hook is used to access the Firestore section of the Firebase SDK. We have to make sure to import the Firestore section of the SDK by calling import "firebase/firestore” (in the next example this is handled automatically through Suspense)
  3. Next we create a reference to the projects Firestore collection by calling firestore.collection("projects") — this will be used later to attach a listener
  4. The useFirestoreCollectionData accepts a ref as an argument and attaches a listener which streams changes directly to your component — this is where data, loading state, and error state come from.
  5. The rest of the component is different if conditions which render different content based on state

Suspense

As noted before, it would be great if we could leverage Suspense to make loading logic more clear to reason about. Managing async logic this way also makes it makes it easy to group other async tasks with data loading such as lazy loading other components and/or sections of the Firebase SDK. This will help cut down on initial load time of your applications by making your bundles smaller.

Whats Happening

  1. The top level of the App is setup with FirebaseAppProvider — this is a Context provider for storing the firebase SDK instance. Since this is a provider, children within it will be rendered and will have the ability to use the values of that context. In this example we pass the props suspense to the provider to enable suspense mode in reactfire.
  2. Next we see ErrorBoundary — this is a React Error Boundary for catching errors which are thrown in child components (this will handle errors in our listener). react-error-boundary library is used to avoid creating a custom class component
  3. Within the boundary we seeSuspenseWithPerf — this is a version of React’s Suspense which also has built in logging to Firebase Performance. Here are using SuspenseWithPerf to show a loading message, whileProjects suspends (since it is the only child component).
  4. In the Projects component the useFirestore hook is used to lazy load the Firestore section of the Firebase SDK. This will cause the component to suspend only if the Firestore section has not already been loaded.
  5. Next we create a reference to the projects Firestore collection by calling firestore.collection("projects") — this will be used later to attach a listener
  6. The useFirestoreCollectionData hook suspends while a listener is attached which streams changes directly to your component — the changes will cause your component to re-render. Since the loading state is handled with Suspense, only data needs used (unlike the first example).

Are You Sure? What if I wanted data in multiple places?

Let Firebase’s SDK handle it for you!

Firebase’s SDK is smart enough to know if you have multiple listeners with the same query. This means that we can load data within multiple components at different levels of the component tree.

This is a little easier to prove that this is happening with Real Time Database since you can run the database profiler that comes with firebase-tools:

firebase database:profile

Now, regardless of how many components which have a listener attached to a path, there will only be one listener request logged with with database. This is the client being smart enough to update all listener callbacks for any queries which are the same!

NOTE: The above examples use Firestore and will need to be updated to use Real Time Database in order to show up in the profiler since the profiler only works for Real Time Database.

How do I start a new project using these patterns?

generator-react-firebase is a Yeoman generator that I created which outputs a material-ui + Firebase project with a simple projects list view with project detail page. It has been updated to use reactfire in the project by default — there is still the option to opt into using redux, but it is no longer the default.

To use the generator, you install it, then run it. You will prompted to select features you like, then to provide information needed to setup your project:

npm install -g yo generator-react-firebase
yo react-firebase

Things will wrap up by automatically installing the apps dependencies with either yarn or npm — once that is done you can run yarn start.

Other Questions That May Come To Mind

Do I need to be using Concurrent Mode to use Reactfire?

No. You can opt out of enabling suspense mode all together.

Even if you enable suspense mode in reactfire, you do not need to use React’s Concurrent Mode.

Even though suspense mode is “meant for React Concurrent Mode” doesn’t mean that you required to use a version of React that has concurrent mode for things to work. I personally have used Reactfire mostly with both react 16.18 and 17 without React’s concurrent mode enabled (even enabled in reactfire) and have not noticed any issues yet.

Should I switch my old projects to use these new patterns?

The answer is most likely no, but that will depend on the project. The decision will have to consider how large the project is, potential impact to application users as well as developers, and value added by making the shift.

I personally switched fireadmin.io (source here) over to these patterns, but that is because of a number of upsides including:

  • Real world side by side comparison of using redux and using contexts with a tool like reactfire
  • Help understandability and maintainability of the project for open source contributors

What if I still want to use Redux?

More power to you! It can be a great tool for plenty of use cases, just remember to choose tools based on the needs of the project.

react-redux-firebase and redux-firestore will still be up-kept since there are plenty of projects which require Redux and/or can benefit from loading database data into a store.

Though it is no longer the default, generator-react-firebase still has an option to include Redux. When choosing this option, react-redux-firebase is included and if you choose to include Firestore, redux-firestore will also be included.

What if I don’t want Firebase specific logic all throughout my application?

Using custom hooks can help separate all of this logic out into a business/application specific domain. For example, if your application has projects you could make a useProjects custom hook:

Author of react-redux-firebase and generator-react-firebase. Mechanical/Aerospace Engineer turned full-stack JS engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store