DEV Community

Rakan Nimer
Rakan Nimer

Posted on

Declaratively Render Your Firebase Auth, Database & Firestore Data with @react-firebase.

TLDR;

@react-firebase was released to make integrating Firebase in your React App fast & easy ๐ŸŽ‰.

If you use React & Firebase, go check it out !

The longer version

I โค๏ธ Firebase & React but getting them to work together on relatively large codebases with other contributors tends to lead to :

  1. ๐Ÿ Un-necessary complexity in your codebase
  2. ๐Ÿ› Subtle bugs with subscription management and inter-resource dependencies.
  3. ๐ŸŽƒ Zombie listeners sticking around when your UI doesn't need them.

@react-firebase offers small, typed, well-tested composable components that 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/

It 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 only need to pass in your version of firebase as a prop (the firebase app is already initialized on 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 a React Node or a sync function that returns a 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 isSigned ? (
      <pre style={{ height: 300, overflow: "auto" }}>
        {JSON.stringify({ isSignedIn, user, providerId }, null, 2)}
      </pre>
    ) : <div>Not signed in.</div>;
  }}
</FirebaseAuthConsumer>
Auth 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 }) => {
    return (
      <pre>
        {JSON.stringify({ value, isLoading })}
      </pre>
    );
  }}
</FirebaseDatabaseNode>

You can also query your Firebase data by using all supported Firebase Realtime Database operators :

<FirebaseDatabaseNode path={"user_bookmarks/BOOKMARK_ID"} limitToFirst={10} orderByKey>
  {({ value, isLoading }) => {
    return (
      <pre>
        {JSON.stringify({ value, isLoading })}
      </pre>
    );
  }}
</FirebaseDatabaseNode>

All supported 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={`/user_bookmarks/id`}>
    {({ 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

Related Work

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

Here are a couple of additional resources just for you :

Top comments (1)

Collapse
 
onar profile image
๐Ÿพ Onar A. • Edited

Hi Rakan,
Thanks for the helpful article.

I noticed in this section it should be return isSignedIn instead of isSigned.

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