18
Neat User and Profile React Hooks for Firebase V9
For a good while, I've been relying on a little package called react-firebase-hooks to turn my Firebase realtime document fetching into tidy hooks. However, this library has become a bit sketchy since the release of Firebase v9 a.k.a the Modular edition.
So, I decided to remove the dependency and write my own little hooks for the two most common types of Firebase objects I use - Users and Profiles (a collection in Firestore).
The User hook is pretty straightforward. Firebase already gives us access to a handy function called onAuthStateChanged
- an observer for changes to the user's "signed-in" state. It will either return a User object or null
, which is perfect for passing straight into a setUser
function.
The updateUser
function is a bit trickier. I wanted to have a single function that I could pass a displayName
, email
, password
or whatever other properties we could store on a User object. Firebase actually has different update functions for each of these. In the new V9, they're aptly named updateEmail
, updatePassword
and then updateProfile
for everything else (name, photo, etc.)
Combining all this, we get the following hook!
import {
getAuth,
onAuthStateChanged,
signOut,
updateProfile,
updateEmail,
updatePassword,
} from 'firebase/auth';
import { useState } from 'react';
import type { User } from 'firebase/auth';
type UpdateUserProps = {
displayName?: User['displayName'];
photoURL?: User['photoURL'];
email?: User['email'];
password?: string | null;
};
const useUser = (): {
user?: User | null;
updateUser: (props: UpdateUserProps) => Promise<void>;
logout: () => Promise<void>;
} => {
const auth = getAuth();
const [user, setUser] = useState<User | null>(auth.currentUser);
onAuthStateChanged(auth, setUser);
const updateUser = async ({
displayName,
photoURL,
email,
password,
}: UpdateUserProps) => {
if (!user) {
return;
}
if (displayName) {
await updateProfile(user, { displayName });
}
if (photoURL) {
await updateProfile(user, { photoURL });
}
if (email) {
await updateEmail(user, email);
}
if (password) {
await updatePassword(user, password);
}
};
const logout = async () => {
await signOut(auth);
};
return { user, updateUser, logout };
};
export default useUser;
Since Firebase Users can only store high-level account and authentication info such as an email, phone number and photo, it's common practice to create a Profile
collection in Firestore that contains any other info you want to store in relation to a particular user. It's also common practice to use the format users/${user.uid}
for the collection path, so we'll make sure to accept a User object as a prop.
Anyway, let's talk about fetching data from Firestore. The new V9 has a handy function called onSnapshot
that attaches a listener for DocumentSnapshot
events, which is a fancy way of saying it subscribes to a collection and listens for updates. That function takes a document reference (or query), a "next" callback (for success) and an "error" callback. It also takes an "onComplete" callback but since the snapshot stream never ends, it's never called, so 🤷♀️.
The easiest way to manage all this is to stuff it inside a useEffect
function, remembering to cleanup your snapshot at the end (it returns an unsubscribe function 👍). For the dependency array, we want to pass the User's UID so it re-runs every time the user changes (which is handy for clearing the profile data when the user logs out).
Chuck in a loading states, some basic error handling and we've got ourselves a pretty neat profile hook!
import type { User } from 'firebase/auth';
import { getApp } from 'firebase/app';
import { doc, updateDoc, getFirestore, onSnapshot } from 'firebase/firestore';
import type { FirestoreError } from 'firebase/firestore';
import { useEffect, useState } from 'react';
// Whatever your profile looks like!
export type ProfileProps = {};
type UseProfileResponse = {
profile: ProfileProps | null | undefined;
updateProfile: (newData: Partial<ProfileProps>) => Promise<void>;
profileLoading: boolean;
profileError: FirestoreError | undefined;
};
const useProfile = (
user: Partial<User> | null | undefined
): UseProfileResponse => {
const app = getApp();
const firestore = getFirestore(app);
const [profile, setProfile] = useState<ProfileProps | null>(null);
const [profileError, setProfileError] = useState<
FirestoreError | undefined
>();
const [profileLoading, setProfileLoading] = useState(false);
useEffect(() => {
if (!user?.uid) {
setProfile(null);
return undefined;
}
setProfileLoading(true);
const profileRef = doc(firestore, 'users', user.uid);
const unsubscribe = onSnapshot(
profileRef,
(profileDoc) => {
setProfile(profileDoc.data() as ProfileProps);
setProfileLoading(false);
},
setProfileError
);
return unsubscribe;
}, [firestore, user?.uid]);
const updateProfile = async (
newData: Partial<ProfileProps>
): Promise<void> => {
if (!user?.uid) {
return;
}
const profileRef = doc(firestore, 'users', user.uid);
await updateDoc(profileRef, newData);
};
return {
profile,
updateProfile,
profileLoading,
profileError,
};
};
export default useProfile;
Anyway, that's it for today. Happy hooking (and also Christmas 🎄🎅).
18