Reputation: 978
I'm building a workout program planner app, the workout program is handled in the app with a SetProgram context and is updated with a custom hook called useProgram. I need that when the user logins that the app will fetch data from firestore and display the user's workout program, how can I do this? Keeping in mind that the useProgram hook is also used throughout the app to edit and update one's workout program.
App.tsx
import React, { useContext, useEffect, useState } from "react";
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from "./Router";
import FirebaseApp from "./firebase";
import SetProgram from "./context/program";
import { useProgram } from "./components/hooks/useProgram";
import firebaseApp from "./firebase/firebase";
import { useAuthState } from "react-firebase-hooks/auth";
function App() {
const program = useProgram();
const day = useDay();
const [user, loading, error] = useAuthState(firebaseApp.auth);
return (
<div className="App">
<SetProgram.Provider value={program}>
<Router>
<AppRouter />
</Router>
</SetProgram.Provider>
</div>
);
}
export default App;
firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import firebaseConfig from "./config";
class Firebase {
auth: firebase.auth.Auth;
user: firebase.User | null | undefined;
db: firebase.firestore.Firestore;
userProgram: {} | undefined;
constructor() {
firebase.initializeApp(firebaseConfig);
this.auth = firebase.auth();
this.db = firebase.firestore();
}
async register() {
if (this.user) {
this.db.collection("users").doc(this.user.uid).set({
name: this.user.displayName,
email: this.user.email,
userId: this.user.uid,
program: {},
});
}
}
async getResults() {
return await this.auth.getRedirectResult().then((results) => {
console.log("results.user", results.user);
if (!results.additionalUserInfo?.isNewUser) {
this.getProgram();
} else {
this.register();
}
});
}
async login(
user: firebase.User | null | undefined,
loading: boolean,
error: firebase.auth.Error | undefined
) {
const provider = new firebase.auth.GoogleAuthProvider();
return await this.auth
.signInWithRedirect(provider)
.then(() => this.getResults());
}
async logout() {
return await this.auth.signOut().then(() => console.log("logged out"));
}
async updateProgram(user: firebase.User, program: {}) {
if (this.userProgram !== program) {
firebaseApp.db
.collection("users")
.doc(user.uid)
.update({
program: program,
})
.then(() => console.log("Program updated successfully!"))
.catch((error: any) => console.error("Error updating program:", error));
} else {
console.log("No changes to the program!");
}
}
async getProgram() {
firebaseApp.db
.collection("users")
.doc(this.user?.uid)
.get()
.then((doc) => {
console.log("hello");
if (doc.exists) {
this.userProgram = doc.data()?.program;
console.log("this.userProgram", this.userProgram);
} else {
console.log("doc.data()", doc.data());
}
});
}
}
const firebaseApp = new Firebase();
export default firebaseApp;
programContext.tsx
import React from "react";
import Program, { muscleGroup, DefaultProgram } from "../interfaces/program";
export interface ProgramContextInt {
program: Program | undefined;
days: Array<[string, muscleGroup]> | undefined;
setProgram: (p: Program) => void;
}
export const DefaultProgramContext: ProgramContextInt = {
program: undefined,
days: undefined,
setProgram: (p: Program): void => {},
};
const ProgramContext = React.createContext<ProgramContextInt>(
DefaultProgramContext
);
export default ProgramContext;
useProgram.tsx
import React from "react";
import {
ProgramContextInt,
DefaultProgramContext,
} from "../../context/program";
import Program, { muscleGroup } from "../../interfaces/program";
import { useAuthState } from "react-firebase-hooks/auth";
import firebaseApp from "../../firebase";
export const useProgram = (): ProgramContextInt => {
const [user] = useAuthState(firebaseApp.auth);
const [program, setEditedProgram] = React.useState<Program | undefined>();
const [days, setProgramDays] = React.useState<
[string, muscleGroup][] | undefined
>(program && Object.entries(program));
const setProgram = React.useCallback(
(program: Program): void => {
firebaseApp.updateProgram(user, program);
setEditedProgram(program);
setProgramDays(Object.entries(program));
},
[user]
);
return {
program,
days,
setProgram,
};
};
Upvotes: 3
Views: 1007
Reputation: 3189
There are two ways to handle this in my opinion:
ProgramContext
to make sure that the user is logged inApp
or any other entry point from whence you need to make sure that the user is logged in, in a separate UserContextProvider
Let's talk about the latter method, where we can wrap in a separate context called UserContext
. Firebase provides us a listener called onAuthStateChanged
, which we can make use of in our context, like so:
import { createContext, useEffect, useState } from "react";
import fb from "services/firebase"; // you need to define this yourself. It's just getting the firebase instance. that's all
import fbHelper from "services/firebase/helpers"; // update path based on your project organization
type FirestoreDocSnapshot = firebase.default.firestore.DocumentSnapshot<firebase.default.firestore.DocumentData>;
const UserContext = createContext({ user: null, loading: true });
const UserContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const userContext = { user, loading };
const updateUser = (snapShot: FirestoreDocSnapshot) => {
setUser({
id: snapShot.id,
...snapShot.data,
});
};
const authStateListener = async (authUser: firebase.default.User) => {
try {
if (!authUser) {
setUser(authUser);
return;
}
const fbUserRef = await fbHelper.findOrCreateFirestoreUser(authUser)
if ("error" in fbUserRef) throw new Error(fbUserRef?.error);
(fbUserRef as FirestoreUserRef).onSnapshot(updateUser)
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
useEffect(() => {
const unSubscribeAuthStateListener = fb.auth.onAuthStateChanged(authStateListener);
return () => unSubscribeAuthStateListener();
}, [])
return (
<UserContext.Provider value={userContext}>
{children}
</UserContext.Provider>
)
};
export default UserContextProvider;
Where the helper can be something like this:
export type FirestoreUserRef = firebase.default.firestore.DocumentReference<firebase.default.firestore.DocumentData>
const findOrCreateFirestoreUser = async (authUser: firebase.default.User, additionalData = {}): Promise<FirestoreUserRef | { error?: string }> => {
try {
if (!authUser) return { error: 'authUser is missing!' };
const user = fb.firestore.doc(`users/${authUser.uid}`); // update this logic according to your schema
const snapShot = await user.get();
if (snapShot.exists) return user;
const { email } = authUser;
await user.set({
email,
...additionalData
});
return user;
} catch (error) {
throw error;
}
};
Then wrap your other context which provides firestore
data, within this UserContextProvider
. Thus whenever you login or logout, this particular listener be invoked.
Upvotes: 1