Skip to content

Instantly share code, notes, and snippets.

@brunoalano
Last active February 28, 2021 21:45
Show Gist options
  • Save brunoalano/7bccbad91c12d8cc5033d4278649a989 to your computer and use it in GitHub Desktop.
Save brunoalano/7bccbad91c12d8cc5033d4278649a989 to your computer and use it in GitHub Desktop.
import { createContext, useEffect, useState, useMemo, useContext } from 'react';
import cookie from 'js-cookie';
import firebase from 'lib/auth/firebase';
import { useDocument } from '@nandorojo/swr-firestore';
import type { UserModel } from 'types/UserModel';
// Create a context that will hold the values that we are going to expose to our components.
// Don't worry about the `null` value. It's gonna be *instantly* overriden by the component below
type UserContextType = {
user: UserModel | null;
error: unknown;
mutate: unknown;
signinWithGoogle: () => Promise<null>;
signInWithEmailAndPassword: (email: string, password: string) => Promise<null>;
signout: () => Promise<null>;
};
export const UserContext = createContext<UserContextType>(null);
// Create a "controller" component that will calculate all the data that we need to give to our
// components bellow via the `UserContext.Provider` component. This is where the Firebase will be
// mapped to a different interface, the one that we are going to expose to the rest of the app.
type UserProviderProps = { children: React.ReactChild | React.ReactChild[] };
export const UserProvider = ({ children }: UserProviderProps): JSX.Element => {
// Handle the Firestore User
const [authFirebaseUser, setAuthFirebaseUser] = useState<firebase.User | null>(null);
const { data: user, mutate, error } = useDocument<UserModel>(
authFirebaseUser ? `users/${authFirebaseUser.uid}` : null,
);
/**
* User provided by Firestore. Create if don't exists.
*
* @param rawUser Firebase returned User
*/
const handleUser = async (rawUser: firebase.User | boolean): Promise<null> => {
if (typeof rawUser !== 'boolean' && rawUser) {
// Parse the Data
const userData = {
id: rawUser.uid,
email: rawUser.email,
token: await rawUser.getIdToken(),
};
const { token, ...userWithoutToken } = userData;
// Store the Firebase Token into the Cookie
cookie.set('chatlog-token', userData, {
expires: 1,
});
// Get and Create/Update the User in Firestore
await firebase.firestore().collection('users').doc(rawUser.uid).set(userWithoutToken, { merge: true });
setAuthFirebaseUser(rawUser);
mutate();
return null;
}
// User not authenticated
setAuthFirebaseUser(null);
cookie.remove('chatlog-token');
mutate();
return null;
};
/**
* Authentication Helpers
*/
const signinWithGoogle = (): Promise<null> => {
return firebase
.auth()
.signInWithPopup(new firebase.auth.GoogleAuthProvider())
.then((response) => handleUser(response.user));
};
const signInWithEmailAndPassword = (email: string, password: string): Promise<null> => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => handleUser(response.user));
};
const signout = (): Promise<null> => {
return firebase
.auth()
.signOut()
.then(() => handleUser(false));
};
// Listen to Firebase Authentication changes
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(handleUser);
return () => unsubscribe();
}, []);
// Make sure to not force a re-render on the components that are reading these values,
// unless the `user` value has changed. This is an optimisation that is mostly needed in cases
// where the parent of the current component re-renders and thus the current component is forced
// to re-render as well. If it does, we want to make sure to give the `UserContext.Provider` the
// same value as long as the user data is the same. If you have multiple other "controller"
// components or Providers above this component, then this will be a performance booster.
const values = useMemo(() => ({ user, mutate, error, signinWithGoogle, signInWithEmailAndPassword, signout }), [
user,
]);
// Finally, return the interface that we want to expose to our other components
return <UserContext.Provider value={values}>{children}</UserContext.Provider>;
};
// We also create a simple custom hook to read these values from. We want our React components
// to know as little as possible on how everything is handled, so we are not only abtracting them from
// the fact that we are using React's context, but we also skip some imports.
export const useUser = (): UserContextType => {
const context = useContext(UserContext);
if (context === undefined) {
throw new Error('`useUser` hook must be used within a `UserProvider` component');
}
return context;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment