Home > Mobile >  React Router Dom & Firebase Auth: history.push changes url but does not render new component when ca
React Router Dom & Firebase Auth: history.push changes url but does not render new component when ca

Time:03-02

What I am trying to do
I'm using Firebase v9 and React-router-dom v5.3.0 to make a sign up form which creates a new account and redirects the user to the home screen when an account is created. The route for the home screen is "/".

The problem
My plan is to call history.push("/") after calling the sign up function, which should take me to the home screen. However, when running the code, history.push("/") only updated the URL and did not redirect me to the home screen. I had to reload the page for the home screen to show up, otherwise I'd just be stuck in the sign up form. I have been fiddling with the code and what surprises me is when I remove await signup(email, password), history.push works just as intended. I suspect this behavior has something to do with firebase's sign up function, but I don't know what it is. Can someone offer an explanation?

The code
Here's my code for the sign up form:

import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";

function Signup() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [cfPassword, setCfPassword] = useState("");
    const {signup} = useAuth();
    const history = useHistory();

    async function handleSubmit(e) {
        e.preventDefault();
        if (password !== cfPassword) {
            console.log("Passwords do not match!");
            return;
        }
        try {
            await signup(email, password);
            history.push("/"); // Problematic code is here. This works fine when I remove the previous line.
        } catch (error) {
            console.log(error.message);
        }
    }
    
    return (
        <div>
            <h1>Create an account</h1>
            <form onSubmit={handleSubmit}>
                <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email"/>
                <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password"/>
                <input type="password" value={cfPassword} onChange={(e) => setCfPassword(e.target.value)} placeholder="Confirm Password"/>
                <input type="submit" value="Sign up"/>
            </form>
        </div>
    );
}

export default Signup;

The code containing the authentication context and the related function for signing up:

import { auth } from "../firebase";
import { useState, useEffect, useContext, createContext } from "react";
import { createUserWithEmailAndPassword } from "firebase/auth";

const AuthContext = createContext();

export function useAuth() {
    return useContext(AuthContext);
}

export function AuthProvider({children}) {
    const [currentUser, setCurrentUser] = useState();
    const [isLoading, setIsLoading] = useState(true);

    function signup(email, password) {
        return createUserWithEmailAndPassword(auth, email, password);
    }

    const value = {
        currentUser,
        signup
    }

    useEffect(() => {
        const unsubscriber = onAuthStateChanged(auth, (user) => {
            setIsLoading(true);
            setCurrentUser(user);
            setIsLoading(false);
        });
        return unsubscriber;
    }, []);

    return (
        <AuthContext.Provider value={value}>
            {!isLoading && children}
        </AuthContext.Provider>
    );
}

My App.jsx component containing the router.

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import Home from './pages/mains/home/Home';
import Signup from './components/Signup';

function App() {
    return (
        <AuthProvider>
            <Router>
                <Switch>
                    <Route path="/" exact component={Home} />
                    <Route path="/signup" component={Signup} />
                </Switch>
            </Router>
        </AuthProvider>
    )
}

export default App;

CodePudding user response:

So, after testing and revising my code, I found out that the problem lies in setIsLoading(true) in AuthProvider's useEffect. Remove that line and everything works like a charm. Hopefully this helps someone experiencing the same problem. :)

CodePudding user response:

Issue

Yes, I see now where an issue lies with using the isLoading state to conditionally render the entire app. After creating a new user it seems the onAuthStateChanged handler is invoked and current user is reloaded. When toggling the isLoading state true the AuthContext's children are unmounted.

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState();
  const [isLoading, setIsLoading] = useState(true);

  function signup(email, password) {
    return createUserWithEmailAndPassword(auth, email, password);
  }

  const value = {
    currentUser,
    signup
  }

  useEffect(() => {
    const unsubscriber = onAuthStateChanged(auth, (user) => {
      setIsLoading(true); // <-- setting true
      setCurrentUser(user);
      setIsLoading(false);
    });
      return unsubscriber;
  }, []);

  return (
    <AuthContext.Provider value={value}>
      {!isLoading && children} // <-- unmounts children
    </AuthContext.Provider>
  );
}

Solution

A more practical solution is to add the isLoading state to the context value and create a protected route component that handles conditionally rendering null or a loading indicator, or the routed component or a redirect to log in.

Example:

export function AuthProvider({ children }) {
  const [currentUser, setCurrentUser] = useState();
  const [isLoading, setIsLoading] = useState(true);

  function signup(email, password) {
    return createUserWithEmailAndPassword(auth, email, password);
  }

  useEffect(() => {
    const unsubscriber = onAuthStateChanged(auth, (user) => {
      setIsLoading(true);
      setCurrentUser(user);
      setIsLoading(false);
    });

    return unsubscriber;
  }, []);

  const value = {
    currentUser,
    isLoading,
    signup
  }

  return (
    <AuthContext.Provider value={value}>
      {children} // <-- keep children mounted!
    </AuthContext.Provider>
  );
}

...

const ProtectedRoute = props => {
  const location = useLocation();
  const { currentUser, isLoading } = useAuth();

  if (isLoading) {
    return null;
  }

  return currentUser
    ? <Route {...props} />
    : (
      <Redirect
        to={{
          pathname: "/login",
          state: { from: location } // <-- used to redirect back after auth
        }}
      />
    );
};

...

function App() {
  return (
    <AuthProvider>
      <Router>
        <Switch>
          <ProtectedRoute path="/somethingToProtect" component={<Private />} />
          <Route path="/login" component={Login} />
          <Route path="/signup" component={Signup} />
          <Route path="/" component={Home} />
        </Switch>
      </Router>
    </AuthProvider>
  )
}
  • Related