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>
)
}