João Pedro
João Pedro

Reputation: 978

How to set the default react context value as data from firestore?

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

Answers (1)

Yedhin
Yedhin

Reputation: 3189

There are two ways to handle this in my opinion:

  1. Update the ProgramContext to make sure that the user is logged in
  2. Wrap the App 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

Related Questions