A Composable, Component based React API over Firebase services.

TLDR;

I just released @react-firebase to make integrating Firebase in your React App fast & easy ๐ŸŽ‰. If you use React & Firebase, go check it out !

The problem

I โค๏ธ Firebase & I โค๏ธ React.

I use both extensively at work and in side-projects.

But getting them to work together on large codebases with many contributors, would lead to :

  1. Un-necessary complexity ( Where is this component loading its data from ๐Ÿค” ? )
  2. Subtle bugs with subscription management. ๐Ÿ›
  3. Zombie listeners sticking around when your UI doesn't need them. ๐ŸŽƒ

This solution

@react-firebase offers small, typed, well-tested composable components which allow easy integration of Firebase Auth, Realtime Database and/or Firestore in your React or React Native app.

Overview

@react-firebase/auth, @react-firebase/database, @react-firebase/firestore export Provider and Consumer Components.

Providers

The Provider components take in firebase and firebase config options props and should be rendered once in your app as an ancestor to your Consumer components.

How do I get my Firebase config ?
Web or React Native with Expo

If you're using firebase

Change PROJECT_NAME to your project name and grab your firebase config here :

https://console.firebase.google.com/project/PROJECT_NAME/settings/general/

Your config file should look something like this :

// Firebase Config
const config = {
  apiKey: "API_KEY",
  projectId: "PROJECT_ID",
  databaseURL: "DATABASE_URL",
  authDomain: "AUTH_DOMAIN",
  // OPTIONAL
  storageBucket: "STORAGE_BUCKET",
  messagingSenderId: "MESSAGING_SENDER_ID"
};

React Native Firebase

If you're using react-native-firebase then you don't need to pass any other prop because the firebase app is initialized from the native side.

Provider Examples

import * as firebase from "firebase/app";

import "firebase/auth";
import { FirebaseAuthProvider } from "@react-firebase/auth";

// And or
import { FirebaseDatabaseProvider } from "@react-firebase/database";
import "firebase/database";

// And or
import "firebase/firestore";
import { FirestoreProvider } from "@react-firebase/firestore";
<FirebaseAuthProvider firebase={firebase} {...config}>
  <MyApp1 />
  <MyApp2 />
</FirebaseAuthProvider>

Consumers

Every Module exports Consumer components that will consume and react (๐Ÿ˜…) to Firebase changes.

All Consumer Components expect their children prop to be any valid React Node or any sync function that returns any valid React Node. (library-specific examples below ๐Ÿ‘‡)

Firebase Auth Consumers

FirebaseAuthConsumer

Anywhere inside your app component tree, add a FirebaseAuthConsumer to listen and update the ui when the user's auth status changes.

<FirebaseAuthConsumer>
  {({ isSignedIn, user, providerId }) => {
    return (
      <pre style={{ height: 300, overflow: "auto" }}>
        {JSON.stringify({ isSignedIn, user, providerId }, null, 2)}
      </pre>
    );
  }}
</FirebaseAuthConsumer>

Firebase Auth Helpers Components

The following are tiny wrapper components over FirebaseAuthConsumer that do common things you might want in an app.

IfFirebaseAuthed Example :

<IfFirebaseAuthed>
  {() => {
    return <div>You are authenticated</div>;
  }}
</IfFirebaseAuthed>

Or

<IfFirebaseAuthed>
  <AppWhenAuthed />
</IfFirebaseAuthed>

IfFirebaseUnAuthed

<IfFirebaseUnAuthed>
  () => (
  <div>
    <p>You are not authenticated. This div will disappear when you login.</p>
  </div>
  )
</IfFirebaseUnAuthed>

Or

<IfFirebaseUnAuthed>
  <AppWhenUnAuthed />
</IfFirebaseUnAuthed>

And sometimes you might want to add additional restrictions to some of your UI. For example, you want to show something only if the user is anonymously un-authenticated, or only if they have a certain email or provider.

For that you can use :

IfFirebaseAuthedAnd

<IfFirebaseAuthedAnd filter={({ providerId }) => providerId !== "anonymous"}>
  {({ isSignedIn, user, providerId }) => {
    return (
      <div>
        <p>Hello {JSON.stringify(user)}</p>
        <p>You are authenticated with {providerId}</p>
      </div>
    );
  }}
</IfFirebaseAuthedAnd>

Or

const AppWhenAuthedAndAnonymous = ({ isSignedIn, user, providerId }) => (
  <div>
    <p>Hello {JSON.stringify(user)}</p>
    <p>You are authenticated with {providerId}</p>
  </div>
);

Then in an ancestor's render method :

<IfFirebaseAuthedAnd filter={({ providerId }) => providerId !== "anonymous"}>
  <AppWhenAuthedAndAnonymous />
</IfFirebaseAuthedAnd>

API Reference

Firebase Database Consumers

Read Data
FirebaseDatabaseNode

Anywhere inside your app component tree, add a FirebaseDatabaseNode to listen and update the ui when the data changes at the provided path.

<FirebaseDatabaseNode path={"user_bookmarks/BOOKMARK_ID"}>
  {({ value, isLoading, firebase }) => {
    return (
      <pre style={{ height: 300, overflow: "auto" }}>
        {JSON.stringify({ value, isLoading }, null, 2)}
      </pre>
    );
  }}
</FirebaseDatabaseNode>

All Props.

Write Data
FirebaseDatabaseMutation & FirebaseDatabaseTransaction

The mutation API is inspired by apollo-graphql mutations.

The mutation components inject a runMutation or runTransaction prop, respectively to their children.

FirebaseDatabaseMutation, in addition to the path prop requires a mutation type prop that can be one of :

"set" | "update" | "push";

FirebaseDatabaseMutation Example.

<FirebaseDatabaseMutation path={collectionPath} type="push">
  {({ runMutation }) => (
    <button
      onClick={async () => {
        const { key } = await runMutation(testDocValue);
        if (key === null || typeof key === "undefined") return;
        this.setState(state => ({
          keys: [...state.keys, key]
        }));
      }}
    >
      add-node-with-generated-key
    </button>
  )}
</FirebaseDatabaseMutation>

FirebaseDatabaseTransaction Example.

<FirebaseDatabaseTransaction path={path}>
  {({ runTransaction }) => {
    return (
      <div>
        <button
          onClick={() => {
            runTransaction({
              reducer: val => {
                if (val === null) {
                  return 1;
                } else {
                  return val + 1;
                }
              }
            }).then(() => {
              console.log("Ran transaction");
            });
          }}
        >
          Click me to run a transaction
        </button>
      </div>
    );
  }}
</FirebaseDatabaseTransaction>

Firestore Database Consumers

Read Data with FirestoreDocument & FirestoreCollection

Anywhere inside your app component tree, add a FirestoreDocument or FirestoreCollection to listen to the data and update the ui when it changes.

FirestoreDocument Example.

const s = v => JSON.stringify(v, null, 2);
const App = () => (
  <FirestoreDocument path={path}>
    {({ value, path, isLoading }) => {
      return (
        <div>
          {value && <pre>{s(value)}</pre>}
          {path && <div>{path}</div>}
          <div>{isLoading}</div>
        </div>
      );
    }}
  </FirestoreDocument>
);

FirestoreDocument API

FirestoreCollection Example

<FirestoreCollection path="/user_bookmarks/" limit={5}>
  {d => {
    return <pre>{s(d)}</pre>;
  }}
</FirestoreCollection>

FirestoreCollection API
Write Data with FirestoreMutation & FirestoreTransaction & FirestoreBatchedWrite

The mutation API is inspired by apollo-graphql mutations.

The mutation components FirestoreMutation & FirestoreTransaction inject a runMutation or runTransaction prop, respectively to their children.

FirestoreMutation in addition to the path prop requires a mutation type prop that can be one of :

"set" | "update" | "add";

The BatchedWrite exposes a different API because it behaves differently than writes and transactions, because it can run many updates before committing them.

It injects 2 props, addMutationToBatch and commit, to its children.

  • addMutationToBatch
type addMutationToBatch = (
  {
    path,
    value,
    type
  }: {
    path: string;
    value: any;
    type: "add" | "update" | "set" | "delete";
  }
) => void;
  • commit
type commit = () => Promise<void>;

FirestoreMutation Example

<FirestoreProvider firebase={firebase} {...config}>
  <FirestoreMutation type="set" path={path}>
    {({ runMutation }) => {
      return (
        <div>
          <h2> Mutate state </h2>
          <button
            onClick={async () => {
              const mutationResult = await runMutation({
                nowOnCli: Date.now(),
                nowOnServer: firebase.firestore.FieldValue.serverTimestamp()
              });
              console.log("Ran mutation ", mutationResult);
            }}
          >
            Mutate Set
          </button>
        </div>
      );
    }}
  </FirestoreMutation>
</FirestoreProvider>

FirestoreTranasaction Example

<FirestoreProvider firebase={firebase} {...config}>
  <FirestoreTransaction>
    {({ runTransaction }) => {
      return (
        <div>
          <button
            data-testid="test-set"
            onClick={async () => {
              await runTransaction(async ({ get, update }) => {
                const res = await get({
                  path
                });
                const data = res.data();
                if (typeof data === "undefined") return;
                await update(path, {
                  ...data,
                  a: isNaN(parseInt(data.a, 10)) ? 1 : data.a + 1
                });
              });
            }}
          >
            runTransaction
          </button>
        </div>
      );
    }}
  </FirestoreTransaction>
</FirestoreProvider>

FirestoreBatchedWrite Example

<FirestoreBatchedWrite>
  {({ addMutationToBatch, commit }) => {
    return (
      <div>
        <h2>Batched write</h2>
        <button
          onClick={() => {
            console.log("adding to batch");
            addMutationToBatch({
              path,
              value: { [`a-value-${Date.now()}`]: Date.now() },
              type: "set"
            });
          }}
        >
          Add to batch
        </button>
        <button
          onClick={() => {
            console.log("committing to batch");
            commit().then(() => {
              console.log("Committed");
            });
          }}
        >
          Commit batch
        </button>
      </div>
    );
  }}
</FirestoreBatchedWrite>

Resources

Editable Sandboxes

Verbose Tutorial (WIP)

Bookmarking App Example

React Native with Expo and Firebase Example

React Native Firebase Starter Example

Related Work

Acknowledgements

Thanks for making it to the end of the article โค๏ธ !

Here's a couple of additional resources just for you :