Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Manage Users

This guide leverages the alerting mechanism presented in the Manage client-side state article.

It also assumes you are familiar with:

  • useDocListener
  • useEntity
  • watchEntity

See Document Listeners for more information about these elements of the react-firebase-state library.

The react-firebase-state library provides tools that make it easy to

We discuss these capabilities below.

Listen for changes to the state of the current user

If your application needs information about the current user, you’ll want to define a component that triggers the Firebase Auth listener.

We recommend that you implement a component like FirebaseAuthListener shown below.

// File: ./src/components/FirebaseAuthListener.tsx

import { useAuthLisener, AuthErrorEvent } from "@gmcfall/react-firebase-state";
import { alertError, alertSuccess } from "./Alert/alertApi";

function handleError(event: AuthErrorEvent) {
    const error = event.error;
    alertError(api, "An error occurred while getting information about your account", error);
}

interface FirebaseAuthListenerProps {
    children?: React.ReactNode;
}

export default function FirebaseAuthListener(props: FirebaseAuthListenerProps) {
    const {children} = props;
    useAuthListener({onError: handleError});

    return (
        <>
            {children}
        </>
    )
}

You would use the FirebaseAuthListener component as shown below.

// File: ./src/components/App.tsx

import { FirebaseProvider } from '@gmcfall/react-firebase-state';
import { FirebaseAuthListener } from "./FirebaseAuthListener";

const firebaseApp = initializeFirebaseApp();

export function App() {

    return (
        <FirebaseProvider firebaseApp={firebaseApp}>
            <FirebaseAuthListener>
                { /* Add your child components here*/ }
            </FirebaseAuthListener>
        </FirebaseProvider>
    )
}

You could, of course, put the FirebaseAuthListener lower within your component hierarchy. In general, you want it to wrap any child components that require information about the current user.

Customize the properties of the user

Your application may need to customize the properties of the current user. In this section, we show how to enrich the user data with a Twitter-like handle.

There are four steps involved in this solution.

Define an interface for the enriched user

Let’s call the enriched user a SessionUser. Here’s the interface definition:

// File: ./src/shared/types.ts

import { UserMetadata, UserInfo } from "firebase/auth";

export interface SessionUser {
    // Properties from Firebase User
    emailVerified: boolean;
    isAnonymous: boolean;
    metadata: UserMetadata;
    providerData: UserInfo[];
    refreshToken: string;
    tenantId: string | null;

    /** A Twitter-like handle for the user */
    handle: string;
}

Define a Firebase collection that stores the handles

In our example, we define a Firebase collection named “identities” that stores more than just Twitter-like handles for users. In particular, it stores the new handle property plus the uid and displayName from the Firebase User.

Documents in the “identities” collection conform to the Identity interface shown below.

// File: ./src/shared/types.ts

export interface Identity {
    /** The `uid` property for the user as defined by the Firebase Auth system */
    uid: string;

    /** The `displayName`  for the user as defined by the Firebase Auth system. */
    displayName: string;

    /** A custom Twitter-like handle for the user */
    handle: string;
}

The app must persist these documents as part of the user registration process.

The Identity interface is convenient because the application may need to render the displayName for users other than the current user, and it is non-trivial to extract that information from the Firebase Auth system. (You need to implement a Firebase Function and use the Admin SDK.)

The displayName property defined by the Firebase Auth system may contain the user’s real name, and it may therefore be classified as sensitive information. Similarly, the user may choose a handle that contains personally identifiable information.

If you use the solution presented in this guide in a production system, make sure that you comply with the relevant regulations regarding the protection of personal information.

Define an api for managing the enriched user data

We need to satisfy the following requirements:

  1. The displayName property in Identity documents must remain synchronized with the Firestore User.
  2. Whenever the state of the current User changes, we must update the record of the user in the local cache so that it matches the SessionUser interface.

The following functions support these requirements.

// File: ./src/shared/identity.ts

import { getAuth } from "firebase/auth";
import { getFirestore, doc, updateDoc } from "firebase/firestore";
import { EntityApi, setAuthUser, watchEntity } from "@gmcfall/react-firebase-state";
import { alertError } from "../components/Alert/alertApi";
import { Identity, SessionUser } from "./types";

function createSessionUser(user: User, identity: Identity): SessionUser {
    return {
        emailVerified: user.emailVerified,
        isAnonymous: user.isAnonymous,
        metadata: user.metadata,
        providerData: user.providerData,
        refreshToken: user.refreshToken,
        tenantId: user.tenantId,
        handle: identity.handle
    }
}

/**
 * Update the Firestore `Identity` document for a given user so that the 
 * `displayName` is consistent with Firebase Auth.
 */
async function updateDisplayName(api: EntityApi, user: User) {
    const userUid = user.uid;
    const displayName = user.displayName;
    const db = getFirestore(api.firebaseApp);
    const identityRef = doc(db, "identities", userUid);
    try {
        await updateDoc(identityRef, {displayName});
    } catch (error) {
        const message = "An error occurred while updating your user profile";
        alertError(api, message, error, {userUid});
    }
}

/**
 * A transform for Identity entities which updates the current user entity
 * in the local cache as a side-effect.
 * 
 * This function does not actually transform the Identity entity. It sole purpose
 * is to keep the current user entity up-to-date and consistent with the 
 * SessionUser interface.
 */
function transformIdentity(event: DocChangeEvent<Identity>) {
    const api = event.api;
    const serverData = event.data;
    const auth = getAuth(api.firebaseApp);
    const user = auth.currentUser;
    if (user && user.uid === serverData.uid) {
        if (user.displayName !== serverData.displayName) {
            updateDisplayName(api, user);
        }
        const sessionUser = createSessionUser(user, identity);
        setAuthUser(api, sessionUser);
    }

    return serverData
}

function handleIdentityError(event: DocErrorEvent) {
    const api = event.api;
    const error = event.error;
    const path = event.path;
    const auth = getAuth(api.firebaseApp);
    
    const message = auth.currentUser ?
        "An error occurred while loading your user profile" :
        "An error occurred while loading the profile for another user";

    alertError(api, message, error, {path});
}

export const IDENTITY_OPTIONS = {
    transform: transformIdentity,
    onError: handleIdentityError
}

export function identityPath(userUid: string) {
    return ["identities", userUid];
}

/**
 * Transform the Firebase User into a SessionUser.
 */
export function transformUser(event: UserChangeEvent) {
    const api = event.api;
    const user = event.user;
    const leasee = event.leasee;
    const path = identityPath(user.uid);
    
    const [identity, identityError] = watchEntity(api, leasee, path, IDENTITY_OPTIONS);

    if (identity) {
        if (identity.displayName !== user.displayName) {
            updateDisplayName(api, user);
        }
        return createSessionUser(user, identity);
    } else if (identityError) {
        const message = "Cannot create SessionUser because an error occurred while loading the Identity";
        throw new Error(message, {cause: identityError});
    } else if (identity===null) {
        const message = "Cannot create SessionUser because the Identity document was deleted";
        throw new Error(message);
    } else {
        // The Identity record must be pending. The SessionUser entity will
        // be created and added to the cache by `identityTransform`.
        // For now, we return `undefined` to signal that the SessionUser entity is
        // pending.
        return undefined;
    }
}

Use the api to transform the Firebase User into the enriched user

We need to modify the FirebaseAuthListener component to use the transformUser function. Here’s the revised component:

// File: ./src/components/FirebaseAuthListener.tsx

import { useAuthLisener, AuthErrorEvent } from "@gmcfall/react-firebase-state";
import { alertError, alertSuccess } from "../Alert/alertApi";
import { transformUser } from "../../shared/identity"; // NEW

function handleError(event: AuthErrorEvent) {
    const api = event.api;
    const error = event.error;
    alertError(api, "An error occurred while getting information about your account", error);
}

interface FirebaseAuthListenerProps {
    children?: React.ReactNode;
}

export default function FirebaseAuthListener(props: FirebaseAuthListenerProps) {
    const {children} = props;
    useAuthListener({
        onError: handleError, 
        transform: transformUser // NEW
    });

    return (
        <>
            {children}
        </>
    )
}

The new lines of code are marked with a “NEW” comment.

Get information about the current user

The FirebaseAuthListener component starts a listener that monitors changes to the state of the current user. The following example shows how a component nested within the FirebaseAuthListener can get information about the current user.

// File: ./src/components/CurrentUserIdentity.jsx

import { useAuthUser } from "@gmcfall/react-firebase-state";
import { SessionUser } from "../shared/types";
import { UserIdentity } from "./UserIdentity";

export function CurrentUserIdentity() {
    
    const [, user] = useAuthUser<SessionUser>();

    return (
        user ? (
            <UserIdentity 
                displayName={user.displayName} 
                handle={user.handle}
            />
        ) : null;
    )
}

The useAuthUser hook has a template parameter that allows you to specify the type of the current user. In this example, we have SessionUser as the value of the template parameter.

If you have not enriched the user entity with additional properties, you can omit this parameter. The useAuthUser hook returns the Firebase User by default.

For completeness, here’s the definition of the UserIdentity component:

// File: ./src/components/UserIdentity.tsx

interface UserIdentityProps {
    displayName: string;
    handle: string;
}

export function UserIdentity(props: UserIdentityProps) {
    const {displayName, handle} = props;

    return (
        <span>{handle} ({displayName})</span>
    )
}

Get information about any user

As we discussed earlier, you can always get information about users other than the current user by creating a Firebase Function that leverages the Admin SDK.

However, if you maintain a Firestore collection that replicates (and possibly enriches) user data, then it is easier to fetch that data as illustrated by the following component.

// File: ./src/components/AnyUserIdentity.tsx

import { useDocListener } from "@gmcfall/react-firebase-state";
import { Identity } from "../shared/types";
import { identityPath, IDENTITY_OPTIONS} from "../shared/identity";
import { UserIdentity } from "./UserIdentity";

interface AnyUserIdentityProps {
    userUid: string;
}

export function AnyUserIdentity(props: AnyUserIdentityProps) {
    const {userUid} = props;
    const path = identityPath(userUid);
    const [, identity] = useDocListener("AnyUserIdentity", path, IDENTITY_OPTIONS);

    return (
        identity ? (
            <UserIdentity
                displayName={identity.displayName}
                handle={identity.handle}
            />
        ) : null;
    )
}