Chapter 19 Firebase

This chapter discusses how to integrate and use the Firebase web service in a client-side application (using React). Firebase is a web service that provides tools and infrastructure for use in creating web and mobile apps that store data online in the cloud (a “web backend solution”). In effect, Firebase acts as a server that can host information for you, so that you can just build client-side applications without needing to program a web server.

  • Firebase is owned and maintained by Google, but will still work perfectly fine with Facebook’s React framework.

In particular, this chapter will discuss two features of Firebase:

  1. The Firebase service also provide a number of “database” systems. This chapter describes how to use the realtime database, which provides a simple NoSql-style database: you can think of it as a single giant JSON object in the Cloud. Firebase provides methods that can be used to refer to this object and its properties, make changes to the object. Most importantly, it provides the ability to “listen” for changes made by others so that the page can automatically update based on actions taken by other users. In effect you can create a data binding between clients and the server, so that if you update data on one machine, that change automatically appears on another.

    Firebase also offers additional database structures. Firestore is another NoSQL (document-based) database, but focused more on one-time data fetch queries rather than the continuous data binding provided by the realtime database. Firebase Storgae is used for storing larger data elements (anything that can’t be defined in JSON), such as images or videos. This chapter focuses on the Realtime Database as an easier-to-learn system and to practice data binding.

  2. The Firebase service also offers a client-side based system for performing user authentication, or allowing users to “sign in” to your application and have particular information associated with them. It’s possible to have users sign up with an email and password, or even using an external service (e.g., “sign in with Google”). Firebase also provides a way to manage all those extra interactions associated with accounts, like being able to reset passwords or confirm email addresses. And this is all performed securely from the client-side, without the need to set up an additional server to perform OAuth work.

These features mean that Firebase can be a go-to back-end for producing a client-side app that involves persisted data (like user accounts). And its free service tier is perfectly suited for any development app.

This chapter details how to use version 9+ of the Firebase library, released in August 2021—and in particular it’s modular API (which allows for smaller and more efficient bundles when using webpack and React). Note that while this version is conceptually the same as previous versions, the module syntax for calling and importing functions is different. See the upgrade guide for details. Be careful when looking up examples and resources that they’re use the same version as you!

Note that Firebase also provides Firebase Hosting for hosting and serving web applications. This is particularly useful for hosting single-page-applications, such as those built with React Router. Hosting does not require any modifications to your source code, so is not discussed here.

19.1 Setting up Firebase

Because Firebase is a cloud service, you will need to set it up externally in order to use it in your web application. Firebase setup and configuration is handled on Firebase’s website at https://firebase.google.com/, and in particular in the Firebase Web Console where you can manage individual projects.

When developing a Firebase app, keep the Web Console open all the time so you can check on database, user list, etc!

Creating a Project

In order to use Firebase in your web app, you will need to sign up for the service. Visit https://firebase.google.com/ and click the “Get Started” button to do so. You will need to sign in with a Google account (e.g., a UW account if you’ve set up Google integration). Signing up will direct you to the Firebase Web Console.

In the Web Console, you can manage all of your “projects”—one per application you’ve created (think: per repository). Each project will track its own database of information, its own set of users, etc.

You can create a project by clicking the “Add Project” button. On the next screen, you’ll need to give the app a unique name. Naming the project after your app is a good idea. This name is used internally by the Firebase syste; it won’t necessarily be visible to your users, though if you use Firebase Hosting then the project name will be part of the default URL for the application.

  • Recommendation: do not enable Google Analytics for your projects!

Once you’ve created the project, you will be taken to the Web Console for that project. This is a web page where you will be able to manage the configuration of the project, the users who have signed up, and any data in the database. In particular, note the navigation menu on the left-hand side: you will use this to access different parts of your project (Realtime Database to manage the database, and Authentication to manage users).

Firebase navigation menu

Including Firebase in React

In order to use Firebase in a web app (including a React app), you will need to add the Firebase library to your web page, as well as some specify some configuration data to connect to the correct Firebase project.

From the project’s Web Console page, click the button (it look like a </> in the above image) to add Firebase to a web app. This will take you to a new page where you’ll need to put in a nickname for the app and then present some JavaScript code for loading the Firebase library (make sure that “Use npm” is selected—you don’t want to include a <script> element!).

In a React app you you will integrate Firebase directly into your source code by modifying the index.js file:

  1. First, install and Firebase library using npm:

    # Note: do this in your app's repo (the folder with the `package.json` file)
    
    npm install firebase
  2. Then, copy the provided import statement, firebaseConfig variable, and initializeApp() function call into your index.js file. This will specify which Firebase project your web app should connect to whenver you call functions to access Firebase.

    Note that you will need to paste this code before the call ReactDOM.render(). The index.js file will often contain such configuration data that is relevant to the app rather than to a specific component.

19.2 Realtime Database

The Firebase web service provides a realtime database for storing and accessing data in the cloud. You can think of this database as being a single giant JSON object in the Cloud that can be simultaneously accessed and modified by multiple clients—and since each client reads the same database, changes made by one user will be seen by others as real-time updates.

For example, you might have a database that is structured like:

{
  "people" : {
    "amit" : {
      "age" : 35,
      "petName" : "Spot"
    },
    "sarah" : {
      "age" : 42,
      "petName" : "Rover"
    },
    "zhang" : {
      "age" : 13,
      "petName" : "Fluffy"
    }
  }
}

This database object has a people key that refers to an object, which itself contains keys that refer to individual “person” objects (each of which has an age and petName property).

In the Firebase Web Console (under the “Database” tab in the navigation menu), this data structure would be presented as:

A Firebase database with sample people data

  • Note that in the Firebase Web Console you can edit this database directly: viewing, adding, editing, and deleting elements in the JSON. This is useful for debugging—both to check that your code is editing the database accurately, and to clean up any mistakes.

Although the JSON database can have many levels of nested objects, best practice is to try and keep the structure as “flat” as possible and avoid nesting too much data. This avoids you needing to download the nested “details” for a value if you only need to know e.g., the key names. See Structure Your Data for more details and examples.

Setting Up the Database

In order to enable the realtime database for your project, navigate to the console page for the database and click the “Create Database” button.

Make sure you’re creating the Realtime Database, not the Firestore Database!

After selecting your local region, you’ll be prompted to set up the Security Rules. Choose to start in test mode to get started immediately; see the below section for more about security rules.

Security Rules

Because the Firebase database is just a giant JSON object in the cloud and is usable from a client-side system, technically anyone can access it. Each element of the JSON object is accessible via AJAX requests (which are sent via firebase functions).

In order to restrict what clients and access this information (read or write to values in the JSON), Firebase lets you define security rules that specify what users can access which elements. For example, you can make it so that only authenticated users can add new entries the database, or that only specific users can update specific entries (e.g., the comments they wrote on a blog).

  • By default, no one can access or modify the data (in locked mode). If you choose to start your project in test mode, then anyone will be able to access or modify for a limited amount of time.

In order to modify or configure the security rules, navigate to the “Rules” tab of the Realtime Database’s page in the Firebase Web Console. You can modify the rules by editing the presented JSON data; remember to hit “Publish” after you’re done editing.

Firebase Security Rules are defined in JSON using a particular (and somewhat awkard) scheme. Basically, inside the "rules" property you specify a JSON tree that mirrors the structure of your database. But instead of having a value for the keys in your database, you specify an object with ".read" and ".write" properties. The values of these properties are boolean expressions that state whether or not the current user is able to read (access) or write (modify) that value. For more on writing rules, such as how to handle user authorization (only certain users can read/write access) or data validation (making sure all data is written of a certain type), see the official documentation.

Data References

To start working with your database, you will need to get a reference to that value in the database.

You begin by getting a reference to the database itself, using the getDatabase() function provided by Firebase’s database module firebase/database:

//import the function from the realtime database module
import { getDatabase } from 'firebase/database';

// Get a reference to the database service
const db = getDatabase();

Importantly, the db value in the example is not a copy of the data or even the database itself! It’s instead just a reference pointer to where that database lives—you can think of it more like a “URL” for the database.

In order to modify any particular values in the database, you will need to get a reference to that value. This is done by using the ref() function, passing in the database reference and the “path” (a string) to the data value in the database’s JSON:

import { getDatabase, ref } from 'firebase/database';

//get a reference to the database service
const db = getDatabase();

//get reference to the "people"" property in the database
const peopleRef = ref(db, "people")

//get reference to the "sarah" property inside the "people" property in the database
//similar to `database.people.sarah` using dot notation
const sarahRef = ref(db, "people/sarah");

Again, these reference are not the data! They are just reference pointers to where the data lives—like the “URL” for that data value.

The “path” to nested values in the database are written in a URI-style “path notation”, where each nested key is indicated by a slash / (rather than the . in dot notation as you’d normally use). Thus "people/sarah" refers to the sarah key inside of the people key inside of the database; "people/sarah/pet" refers to the pet key inside the sarah key inside the people key—the spot in the database with the value "Rover" in the above example.

Since this path is just a string, you can use string concatenation to construct the path to a particular value you wish to reference (e.g., if you only want to work with the value for a particular person):

//variable name to access
const personName = "Ada";

//access the "ada" property inside of the "people" property inside the database
const personOfInterestRef = ref(db, "people/"+personName);

Alternatively, you can use the child() function to get a reference to a specific “child” element in the database:

import { ref, child } from 'firebase/database';

//equivalent to the above example

//get reference to the "people" property in the database
const peopleRef = ref(db, "people")

//get reference to the "sarah" property inside of the "people" reference'd value
const sarahRef = child(peopleRef, "sarah")

In general, you want to work with a small a subset the data as possible. Thus it’s better to get a reference to "person/sarah" if you’re only going to read or write to that value, rather than get a reference to the whole "people" and then just read/write the sarah key in that.

Writing Data

You can modify values in the database by using the set() function. This function takes two arguments: a database reference, and then the new value you want to assign to that location in the database:

//alias the `set` function as `firebaseSet`
import { ref, set as firebaseSet } from 'firebase/database'

//get a reference to where sarah's age is stored in the database
const sarahAgeRef = ref(db, "people/sarah/age");

//change Sarah's age to 43 (happy birthday!)
const newValueForSarahAge = 43;

//assign the new value in the database
firebaseSet(sarahRef, newValueForSarahAge);

This will change the value in the database at "people/sarah/age" to 43.

Note that in this example, I have use the as keyword to alias the set function when importing it, instead calling it firebaseSet. This is for clarity, as “set” is an overly generic name for a function.

Note that as with normal JSON objects, you can also “set” values for database entries that didn’t previously exist—you do not have to “declare” a value in the database ahead of time

//a reference to the people entry in the database
const peopleRef = ref(db, "people")

//a reference to a location with no currently defined value
const adaRef = ref(peopleRef, "ada");

//set an object to that location in the database
firebaseSet(adaRef, {age: "206", pet: "Charlie"})

Finally, note that the set() method returns a Promise. This you can use .then() (or await) to execute statements after the database has updated, and you can thus catch any errors that may occur when attempting to update a value. This is especially useful for debugging, but also is good for providing user feedback.

const sarahPetRef = ref(db, "people/sarah/pet");

//change Sarah's pet
firebaseSet(sarahPetRef, "Sparky")
    .then(() => console.log("data saved successfully!"))
    .catch(err => console.log(err)); //log any errors for debugging

Listening for Data Changes

Because Firebase’s database is intended for realtime access (that may change over time), you read data from it and establish data binding by registering an event listener to listen for changes to that database. In effect any modifications to the data causes a “database change event” which triggers your listener, allowing you to acccess the new updated data.

You register a database change listener by using the onValue() function. This function takes as arguments a database reference (which value you want to listen for changes to) and a callback function that will be executed in response to the event (similar to the DOM’s addEventListener() function):

import { ref, onValue } from "firebase/database"

//get a reference to a particular value location in the database
const amitRef = ref(db, 'people/amit');

//register a listener for changes to that value location
onValue(amitRef, (snapshot) => {
    const amitValue = snapshot.val();
    console.log(amitValue); //=> e.g., { age: 35, petName: "Spot" }
    //can do something else with amitValue (e.g., assign to a state variable)
});

The callback function will be passed as a parameter the latest snapshot of the data value at the listening location. This is a wrapper around the database’s JSON tree (allowing you to navigate it e.g., with child()). Most commonly, you will want to convert the snapshot into an actual JavaScript object by calling the .val() method on it.

The listener will trigger whenever any child node of the targeted refence location changes. Thus even if just people/amit/pet is updated, the above listener will trigger.

Importantly, because this listener involves network access and can trigger throughout the life of your application, in a React app you will need to register the listener inside an effect hook. The effect hook’s “cleanup function” will also need to remove the listener—otherwise you will have a memory leak and errors if your component gets removed (e.g., by changing routes). The onValue() function returns a new function that can be used to “unregister” the listener:

function MyComponent(props) {
  //effect hook
  useEffect(() => {
    const db = getDatabase();
    const amitRef = ref(db, "people/amit");

    //returns a function that will "unregister" (turn off) the listener
    const unregisterFunction = onValue(amitRef, (snapshot) => {
      const amitValue = snapshot.val();
      //...set state variable, etc...
    })

    //cleanup function for when component is removed
    function cleanup() {
      unregisterFunction(); //call the unregister function
    }
    return cleanup; //effect hook callback returns the cleanup function
  })
}

Note that it is possble to read a single value once (without needing to register a listener) by using the get() method. This method returns a promise that will contain the read snapshot (once it is download). However, the Realtime Database is intended and optimized for data binding and notifications. Thus you should pretty much always be using onValue() to read data from the database. If you find yourself wanting to just read a single value once, you’re not thinking about Firebase correctly!

Firebase Arrays

When working with and storing data in the cloud, we often want to organize that data into lists using arrays. However, Firebase does not directly support support arrays: the JSON object in the sky only contains objects, not arrays! This is because Firebase needs to support concurrent access: multiple people need to be able to access the data at the same time. But since arrays are accessed by index, this can cause problems if two people try to modify the array at the same time.

  • The problem is that with an array, an index number doesn’t always refer to the same element! For example, if you have an array ['a', 'b' 'c'], then index 1 may initially refer to the 'b'. However, if you add an element onto the beginning of the array, then suddenly that 1 index refers to the 'a'. And so if a user was trying to modify the 'b' before their machine was aware of the change, they may end up editing the wrong value! This bug is an example of a race condition, which can occur when two processes are modifying data concurrently (at the same time).

To avoid this problem, Firebase treats all data structures as Objects, so that each value in the JSON tree has a unique key. That way each client will always be modifying the value they expect. However, Firebase does offer a way that you can treat Objects as arrays, in the form of a push()(https://firebase.google.com/docs/reference/js/firebase.database.Reference#push) function that will add a value to an object with an auto-generated key.

import { getDatabase, ref, push as firebasePush } from 'firebase/database';

const db = getDatabase;
const tasksRef = ref(db, "tasks"); //an object of tasks

tasksRef.push( {description:'First things first'} ) //add one task
tasksRef.push( {description:'Next things next'} ) //add another task

This will produce a database with a structure:

{
  "tasks" : {
    "-KyxgJhKOVeAj2ibPxrO" : {
      "description" : "First things first"
    },
    "-KyxgMDJueu17348NxDF" : {
      "description" : "Next things next"
    }
  }
}

Notice how the tasks property is an Object (even though we “pushed” elements into it), and each “task” is assigned an auto-generated key (based on the timestamp it was pushed). You would thus still be able to interact with tasks as if it were an array, but instead of using a value from 0 to length as the index, you’ll use a generated “key” as the index.

Always use the push() function to have Firebase automatically manage data arrays for you. Never try to manually create an “array” by giving values keys “0”, “1”, “2” etc.

Firebase snapshots do support a forEach() function that you can use to iterate through their elements, allowing you to loop through the elements in an array. However, if you want to do something more complex (like map(), filter(), or reduce()), you need an actual array. The best way to get this is use call Objects.keys() on the snapshot.val() in order to get an array of the keys, and then you can iterate/map those (accessing each element in the “array” using bracket notation).

//`tasksSnapshot` is a snapshot of the `tasks` "array"
const allTasksObject = tasksSnapshot.val(); //convert snapshot to value

//an array of the keys in the object ["-KyxgJhKOVeAj2ibPxrO", "-KyxgMDJueu17348NxDF"]
const allTasksKeys = Object.keys(allTasksObject);

//map array of keys into array of tasks
const allTasksArray = allTasksKeys.map((key) => {
    const singleTaskCopy = {...allTasksObject[key]}; //copy element at that key
    singleTaskCopy.key = key; //save the key string as an "id" for later
    return singleTaskCopy; //the transformed object to store in the array
});

allTasksArray.map((taskObject) => { ... })

Note that when looping through an “array”, each element is treated handled separately from its key (the same way that a forEach() loop lets you work with array elements separately from their index). But since you need that key as an identifier in order to ref() and modify the JSON element later, you will need to make sure that you “save” the key in the object as you processing it. Don’t lose your key!.

19.3 User Authentication

Firebase provides the ability to authenticate users: to let people sign up for you application and then to check that they are who they say (e.g., have provided the right password). Firebase supports multiple different forms of authentication: users can sign up with an email and password, log in with a social media service such as Google or Facebook, or even authenticate “anonymously” (so you can at least keep track of different people even if you don’t know who they are).

In order to support user authentication, you will need to enable this feature in the Firebase Web Console. Click on the “Authentication” link in the side navigation menu to go to the authentication management page, then choose “Get started”. Under the “Sign-In Method” tab, you can choose what forms of authentication you wish to enable.

  • For example, click on the “Email/Password” option, and then flip the switch to “Enable” that method. Be sure and Save your changes!

Note that you will be able to use this page to see and manage users who have signed up with your Firebase project. This is where you can see if uses have successfully been created, look up their User UID for debugging, or delete users from the project:

Firebase users as presented in the authentication web console

Signing In with FirebaseUI

It is possible to call functions provided by Firebase in order to create and log in users. However, this would require you to implement the “log in form” on your own—and would only easily support the Email/Password authentication option.

Instead, a nicer approach is to use the FireabaseUI library, also managed by Firebase. This library provides a set of predefined UI elements like a “login form”, including ones which will support different authentication methods.

Example of FirebaseUI authentication

There are different FirebaseUI libraries for different platforms; for React you would use firebaseui-web-react. This is an external library (just like react-router), so needs to be installed separately:

npm install react-firebaseui

The React FirebaseUI library provides a Component called <StyledFirebaseAuth>, which you can render in your page like any other Component. You can think of this Component as the “login form”. A complete example of how this Component is used can be found below, following by an explanation.

//import auth functions and variables from Firebase
import { getAuth, EmailAuthProvider, GoogleAuthProvider } from 'firebase/auth'

//import the component
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';

//an object of configuration values
const firebaseUIConfig = {
  signInOptions: [ //array of sign in options supported
    //array can include just "Provider IDs", or objects with the IDs and options
    GoogleAuthProvider.PROVIDER_ID,
    { provider: EmailAuthProvider.PROVIDER_ID, requiredDisplayName: true },
  ],
  signInFlow: 'popup', //don't redirect to authenticate
  credentialHelper: 'none', //don't show the email account chooser
  callbacks: { //"lifecycle" callbacks
    signInSuccessWithAuthResult: () => {
      return false; //don't redirect after authentication
    }
  }
}

//the React compnent to render
function MySignInScreen() {

  const auth = getAuth(); //access the "authenticator"

  return (
    <div>
      <h1>My App</h1>
      <p>Please sign-in:</p>
      <StyledFirebaseAuth uiConfig={firebaseUIConfig} firebaseAuth={auth} />
    </div>
  );
}

There are a few steps to using this Component:

  • The <StyledFirebaseAuth> requires a uiConfig prop, which is an object containing configuration values for that login form (the object has specific properties which correspond to configuratoin options). The config object is usually defined as a global constant.

    The most important configuration option is the signInOptions, listing the different authentication providers supported (what options should show up in the login form). The IDs for these providers are imported from the 'firebase/auth' library—though they are numeric values, not functions

    Other common/useful options are shown in the example; for a complete list of options see the firebaseui-web documentation.

  • The <StyledFirebaseAuth> also requires a firebaseAuth prop, which is a reference to the “authenticator” service—what will handle the process of the user logging in and out. You can get access to this value via the getAuth() function provided by 'firebase/auth' (similar to how you get access to the Realtime Database via the getDatabase() function).

    Note that the authenticator is a singleton value—you can call getAuth() from multiple modules/files without concern. You do not need to pass it around as a prop.

With this Component, users will be able to “log in”, though see the Managing the User section below. You will be able to see a list of users who have connected to your app in the Firebase Web Console. Note in particular that you can see their uid (unique identifier), which is what is used to distinguish between users. (You can’t use emails as identifiers, because someone who logs in with Facebook won’t provide their email address!).

Pro-tip: You don’t need to come up with real email addresses for testing. Try using a@a.com, b@a.com, c@a.com, etc. Similarly, password works fine for testing passwords (though you should never do that in real life!)

The Firebase Web Console will provide a view of the “user database”—but importantly, this is not the same as the Realtime Database. The user database holds very limited information: the authentication provider, a “display name”, and possibly a “photo url” (e.g., where to find an avatar image). But you cannot store any additional information in the “user database”—not profile images, not preferred names or pronouns used, not favorite fish. Any user-specific information will need to be stored in the Realtime Database instead, usually using a “key” that is the user’s uid. For example, your Realtime Database might store information like:

{
  "userData" : {
    "c9PGijn9Snh2YfPA30Hq0t7jn" : {
      "pronouns" : "they/them",
      "favoriteFish" : "sea bass"
    },
    "Tq8t7ETw54XXiWf7XZdnu0J" : {
      "pronouns" : "he/him",
      "favoriteFish" : "cuttle fish"
    }
  }
}

You would then access this data using the same process as any other data in the realtime database, listening to the reference "userData/"+userId.

In order to sign out a user, you will need to use a built-in Firebase function: signOut():

import { getAuth, signOut } from 'firebase/auth';

const auth = getAuth();

signOut(auth)
    .catch(err => console.log(err)); //log any errors for debugging

Note that the signOut() method returns a Promise, so you can .catch() and display any errors.

Managing the User

Although FirebaseUI will log in a user, you will need to do further work in order for your app to know who that logged in user is.

The authenticator (from getAuth()) will keep track of which Firebase User is currently logged in—and this information persists even after the browser is closed. This means that every time you reload the page, the authenticator will automatically perform the authentication and “re-login” the user.

The recommended way to determine who is currently logged in is to register an event listener to listen for events that occur with the “state” of the authentication changes (e.g., a user logs in or logs out). This event will occur when the page first loads and Firebase determines that a user has previously signed up (the “initial state” is set), or when a user logs out. You can register this listener by using the onAuthStateChanged() function:

//import functions
import { getAuth, onAuthStateChanged } from 'firebase/auth'

const auth = getAuth();

onAuthStateChanged(auth, (firebaseUser) => {
    if(firebaseUser){ //firebaseUser defined: is logged in
        console.log('logged in', firebaseUser.displayName);
        //do something with firebaseUser (e.g. assign to a state variable)
    }
    else { //firebaseUser is undefined: is not logged in
        console.log('logged out');
    }
});

The function takes in two arguments: the authenticator, and a callback function that will be executed whenever the “authentication state” changes (e.g., whenever the user logs in or out). The callback function will be passed an object representing the “Firebase User”. This object contains properties about the user, including:

  • uid, which is the unique id for the user
  • displayName, which is the user’s provider-independent displayable name

The most common practice is to take this passed in object and assign it to a more global variable, such as a state variable in a React function (e.g., setCurrentUser(firebaseUser)).

If the user “logged out”, then the passed in firebaseUser value will be null.

Importantly, because this listener involves network access and can trigger throughout the life of your application, in a React app you will need to register the listener inside an effect hook. The effect hook’s “cleanup function” will also need to remove the listener. The onAuthStateChanged() function returns a new function that can be used to “unregister” the listener:

function MyComponent(props) {
  //effect hook
  useEffect(() => {
    const auth = getAuth();

    //returns a function that will "unregister" (turn off) the listener
    const unregisterFunction = onAuthStateChanged(auth, (firebaseUser) => {
      //handle user state change
      if(firebaseUser){
        //...
      }
    })

    //cleanup function for when component is removed
    function cleanup() {
      unregisterFunction(); //call the unregister function
    }
    return cleanup; //effect hook callback returns the cleanup function
  })
}

Firebase React Hooks

In React systems it is common to render different content depending on whether the user is logged in. This could be done with conditional rendering, or with a “protected route” as described in Client-Side Routing.

However, it takes a few seconds for Firebase to authenticate a user when a page first loads. Thus your conditional rendering will often need to handle 3 cases: when a user is logged in, when a user is not logged in, and when the user status is “loading” and the app hasn’t finished checking yet.

While there are some cludgy workarounds (e.g., use a special value for your state to indicate that a user is “loading”), this is a common enough situation that you can use solutions implemented by other people. One such option is the firebase-react-hooks library. This library provides a simple hook (like useState or useParams) that lets you access the user status. In particular, the useAuthState hook will implement the authentication state listener for you:

import { getAuth } from 'firebase/auth';
import { useAuthState } from 'react-firebase-hooks/auth';

function MyComponent(props) {
  const auth = getAuth();
  const [user, loading, error] = useAuthState(auth);

  if(loading){ //still waiting
    return <p>Initializing user</p>
  }

  if(error) { //error logging in
    return <p>Error: {error}</p>
  }

  if(user) { //user is defined, so logged in
    return <p>Welcome {user.displayName}</p>
  } else { //user is undefined
    return <p>Please sign in</p>
  }

  //...
}

This hook returns an array of 3 values (which are destructured in the above example), indicating the current user (or null for no user), whether the auth state is still loading, and whether there was an error.

While using such a library is not necessary, it is a good example of how React’s architecture allows for external libraries that can help reduce boilerplate code.