reactjsfirebasefirebase-authentication

I need to refresh page to signInWithEmailAndPassword


This is my Login Component:

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import {signInWithEmailAndPassword} from "firebase/auth";
import {auth} from "./firebase.js"

export default LoginComponent = () => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        signInWithEmailAndPassword(auth, email, password).then(() => {
            navigate('/')
        }).catch((error) => {
        });
    };

My AuthProvider:

import {onAuthStateChanged,} from "firebase/auth";
import { createContext, useEffect, useState } from "react";
import PropTypes from "prop-types";
import {auth} from "./firebase.js";

export const AuthContext = createContext(null);

const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [accessToken, setAccessToken] = useState(null);

    useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
            if (currentUser) {
                setAccessToken(await currentUser.getIdToken(true));
            }
            setUser(currentUser);
            setLoading(false);
        });

        return () => unsubscribe();
    }, []);

    const authValue = {
        user,
        loading,
        accessToken,
    };

    return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
};

AuthProvider.propTypes = {
children: PropTypes.node.isRequired,
};

export default AuthProvider;

On my routes page, I am doing this:

import { useContext } from 'react'
import { AuthContext } from "./components/Auth/AuthProvider.js";
import UserProfile from "./components/UserProfile"

const App = () => {
    const { user, loading } = useContext(AuthContext);

    function LandingPageRedirect({ children }) {
        if (loading) {
            return <div>Loading...</div>; 
        }
        if (user) {
            return <>{children}</>
        } else {
            return (
                <Navigate
                to="/landing"
                state={{
                    from: location.pathname,
                }}
                replace
                />
            )
        }
    }

    return (
        <div>
            <BrowserRouter>
                <Routes>
                    <Route path='/' element={
                            <LandingPageRedirect>
                                <Home />
                            </LandingPageRedirect>
                        } 
                    />

However, after sign in I am being sent back to"/landing" rather than "/home". I need to refresh the page in order to be sent to the home page rather than landing page. How do I fix this?


Solution

  • Possibly you navigate to '/' before you set user to state. It can happen because of waiting while promise from getIdToken be resolved or even authStateChange call happens after navigation. I have many solutions for you which you can try. Firstly let's move setUser before that getToken call in case it doesn't help you will find another solution below.

    Like this:

    useEffect(() => {
            const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
                setUser(currentUser);
                if (currentUser) {
                    setAccessToken(await currentUser.getIdToken(true));
                }
                
                setLoading(false);
            });
    
            return () => unsubscribe();
        }, []);
    

    or rewrite it without await.

    useEffect(() => {
      const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
        setUser(currentUser);
        currentUser
          .getIdToken(true)
          .then((token) => {
            setAccessToken(token);
          })
          .catch((error) => {
            console.log(error);
          })
          .finally(() => {
            setLoading(false);
          });
      });
    
      return () => unsubscribe();
    }, []);
    

    Or divide this logic to two different useEffect function calls:

    useEffect(() => {
      const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
        setUser(currentUser);
        setLoading(false);
      });
    
      return () => unsubscribe();
    }, []);
    
    useEffect(() => {
      if (currentUser) {
        currentUser
          .getIdToken(true)
          .then((token) => {
            setAccessToken(token);
          })
          .catch((error) => {
            console.log(error);
          });
      }
    }, [currentUser]);
    

    In case it doesn't work stick with next solution I suggested.

    As you can see in the docs https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#signinwithemailandpassword

    signinwithemailandpassword returns Promise that has user inside

    https://firebase.google.com/docs/reference/js/v8/firebase.auth#usercredential

    So you can setUser in then block.

    So expose your setUser from the authProvider. Modified code:

    const AuthProvider = ({ children }) => {
        const [user, setUser] = useState(null);
        const [loading, setLoading] = useState(true);
        const [accessToken, setAccessToken] = useState(null);
    
        useEffect(() => {
            const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
                if (currentUser) {
                    setAccessToken(await currentUser.getIdToken(true));
                }
                setUser(currentUser);
                setLoading(false);
            });
    
            return () => unsubscribe();
        }, []);
    
        const authValue = {
            user,
            loading,
            accessToken,
            setUser
        };
    
        return <AuthContext.Provider value={authValue}>{children}</AuthContext.Provider>;
    };
    

    Update your handleSubmit

    const { setUser } = useContext(AuthContext);
    ...
    const handleSubmit = (e) => {
            e.preventDefault();
            signInWithEmailAndPassword(auth, email, password).then((res) => {
                setUser(res.user);
                navigate('/')
            }).catch((error) => {
            });
        };
    

    After that the application won't redirect you to landing.