Search code examples
reactjstypescriptreact-hooksreact-context

React context doesn't update value when updated


I'm trying to make a simple routing system. Either the user is authenticated or not. I'm trying to use hooks to achieve this but I'm not having the biggest success with it just yet.

authProvider.tsx

import React, {Dispatch, PropsWithChildren, SetStateAction, useContext, useState} from "react";
import { signInWithEmailAndPassword } from "firebase/auth";
import {ref, child, get} from "firebase/database";
import { database, auth } from "../data/firebase";
import { useNavigate } from "react-router-dom";
import { Admin } from "../types/user.type";

interface AuthContext {
    user: Admin | undefined;
    setUser: Dispatch<SetStateAction<Admin>>;
    authenticateUser: (email: string, password: string) => void;
    authenticated: boolean;
    setAuthenticated: Dispatch<SetStateAction<boolean>>;
}
const AuthContext = React.createContext<AuthContext>({} as AuthContext)

export const useAuth = () => useContext(AuthContext)

export const AuthProvider = ({children}: PropsWithChildren<{}>) => {
    const [user, setUser] = useState<Admin>({} as Admin)
    const [authenticated, setAuthenticated] = useState<boolean>(false)
    const nav = useNavigate();

    const authenticateUser = (email: string, password: string):void=> {

        signInWithEmailAndPassword(auth, email, password)
            .then(async (userCredential) => {
                // Signed in
                const admin =  await fetchUserData(userCredential.user.uid)
                console.log(admin)
                if (admin?.role !== "Admin") return;
                setUser(user);
                setAuthenticated(true)
                return nav("/")
            })
            .catch((error) => {
                const errorCode = error.code;
                const errorMessage = error.message;
            });

    }

    const fetchUserData = (uid: string): Promise<Admin | null> => {
        return new Promise((resolve, reject) => {
            const dbRef = ref(database);

            get(child(dbRef, `users/${uid}`))
                .then((snapshot) => {
                    if (snapshot.exists()) {
                        resolve(snapshot.val() as Admin) ;
                          console.log(snapshot.val());
                    } else {
                        reject("Admin not found.")
                        //   console.log("No data available");
                    }
                })
                .catch((error) => {
                    reject("There was an error fetching the admin data.")
                });
        })
    }


    return (
        <AuthContext.Provider value={{ user, setUser, authenticated, setAuthenticated, authenticateUser }}>
            {children}
        </AuthContext.Provider>
    )
}

App.tsx

import React from "react";
import {Route, Routes, Navigate,BrowserRouter} from "react-router-dom";
import { Home, Login } from "./pages";
import { AuthProvider, useAuth } from "./providers/authProvider";

function App() {
    const {authenticated} = useAuth()
    return (
        <BrowserRouter>
            <AuthProvider>
                {authenticated
                    ? (
                        <Routes>
                            <Route path="/" element={<Home />}/>
                        </Routes>
                    )
                    :(
                        <Routes>
                            <Route path="/login" element={<Login />}/>
                            <Route path="*" element={<Navigate to="/login" />}/>
                        </Routes>
                    )
                }
            </AuthProvider>
        </BrowserRouter>
    );
}

export default App;

I call authenticateUser from my login page. The function gets called as expected and the user object is logged in the console. Have I missed something in the documentation which led to authenticated not being updated in the App.tsx? It also doesn't refresh the page when calling nav("/") after setting authenticated to true.


Solution

  • You are calling the useAuth() Hook which consumes the AuthContext outside of the actual context. In order to get the data from AuthContext you have to wrap it around <App /> in that case.

    const auth = useAuth(); // outside of your authprovider
    
    return (
        <AuthProvider> {/* your context definition component */}
            {/* You can only access the context INSIDE AuthProvider */}
            <ChildrenComponent />
            <ChildrenComponent />
            <ChildrenComponent />
            <ChildrenComponent />
        </AuthProvider>
    )
    

    do this in your index.tsx or whereever you call your app.tsx:

    <AuthProvider>
        <App /> {/* useAuth() has now access to the context */}
    </AuthProvider>