I encountered an issue that I can't solve. In my handleLogIn function within the ContextProvider component, I am using the useNavigate hook to redirect the page to "/" if the user successfully logs in. However, after the user successfully logs in, they are not redirected to the main page; instead, they are redirected back to the login page. After the second form submission, the page redirection works correctly.
How can I redirect the user from the "/sign-in" or "/sign-up" page immediately after form submission?
here is my contextProviderComponent:
export const ContextAPI = createContext<null | TContextAPI>(null);
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<firebase.User | undefined | null>(undefined);
const [currentUserId, setCurrentUserId] = useState<string | undefined | null>(undefined);
const [loading, setLoading] = useState<boolean>(true);
const { signUpValues, SignUpInputConstructor, handleRegister, signUpError } = useAuth();
const { logInValues, SignInInputConstructor } = useLogIn();
const { handleLogout, logOutError } = useLogOut();
const { newGroupName, isShowGroupCreator, setIsShowGroupCreator, setNewGroupName } = useGroupMenu();
const handleUserGroups = () => {
if (currentUserId) {
const groupRef = doc(db, `/user_groups/${currentUserId}/${newGroupName}`, uuid());
if (newGroupName === "" || newGroupName.length > 20) {
console.log("group name is incorrect");
} else {
setDoc(groupRef, { merge: true });
console.log("db updated sucessfully");
navigate("/");
}
} else {
console.log("currentUserId is not set");
}
};
const [logInError, setLogInError] = useState<string>("");
const logIn = (email: string, password: string) => {
return auth.signInWithEmailAndPassword(email, password);
};
const handleLogIn = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
return setLogInError("email is not correct");
}
if (logInValues.password === "") {
return setLogInError("password field can not be empty");
} else {
try {
setLogInError("");
setLoading(true);
logIn(logInValues.email, logInValues.password).then(() => {
navigate("/");
});
} catch (errors) {
setLogInError("Failed to log-in in account");
}
setLoading(false);
}
};
const vals: TContextAPI = {
setNewGroupName,
isShowGroupCreator,
setIsShowGroupCreator,
newGroupName,
currentUserId,
handleUserGroups,
handleLogout,
logOutError,
setLoading,
currentUser,
signUpValues,
SignUpInputConstructor,
handleRegister,
loading,
signUpError,
logInValues,
SignInInputConstructor,
handleLogIn,
logInError,
};
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged((user) => {
setCurrentUser(user);
setLoading(false);
setCurrentUserId(user?.uid || null);
});
return unsubscribe;
}, []);
return <ContextAPI.Provider value={vals}> {!loading && children} </ContextAPI.Provider>;
};
and this is my LogIn component:
const LogIn = () => {
const SignInInputConstructor = useContextSelector(ContextAPI, (v) => v?.SignInInputConstructor);
const loading = useContextSelector(ContextAPI, (v) => v?.loading);
const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
const handleLogIn = useContextSelector(ContextAPI, (v) => v?.handleLogIn);
const logInError = useContextSelector(ContextAPI, (v) => v?.logInError);
return (
<>
<h1>Log In page</h1>
<form
onSubmit={(e) => {
handleLogIn?.(e);
}}
>
{currentUser?.email}
{logInError && <Alert severity="error">{logInError}</Alert>}
{SignInInputConstructor?.map((item) => (
<>
<Typography>{item.typography}</Typography>
<TextField
id={item.id}
placeholder={item.placeholder}
variant={item.variant}
value={item.value}
onChange={item.onChange}
/>
</>
))}
<Button variant="contained" type="submit" disabled={loading}>
Log In
</Button>
<Typography>
Need an account? <Link to={"/sign-up"}>Sign Up</Link>
</Typography>
</form>
</>
);
};
and this is my privateRoute component:
const PrivateRoute = () => {
const currentUser = useContextSelector(ContextAPI, (v) => v?.currentUser);
if (currentUser === undefined) return null;
return currentUser ? <Outlet /> : <Navigate to="/sign-in" />;
};
export default PrivateRoute;
and the lastone is my main component with all routes:
const Main = () => {
const router = createBrowserRouter(
[
{
path: "/",
element: <App />,
children: [
{
path: "/sign-in",
element: <LogIn />,
},
{
path: "/sign-up",
element: <Register />,
},
{
path: "/",
element: <PrivateRoute />,
children: [
{
path: "/",
element: <GroupMenu />,
children: [
{
path: "/add-group",
element: <AddGroupModal />,
},
],
},
{
path: "/group-content",
element: <GroupContent />,
},
{
path: "/dashboard",
element: <Dashboard />,
},
],
},
],
},
],
{ basename: "/thatswhy_items_counter/" }
);
return (
<React.StrictMode>
<ThemeProvider theme={theme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
);
};
If you need more details, here is the project repo on GitHub: repo-link Thanks to all of you for your help in advance!
I've tried to move my login functions from the custom login hook to the ContextProvider component. I've tried to manage the conditional loading state in the PrivateRoute component. I've also tried to update my currentUser state in the logIn function within the ContextProvider component. Unfortunately, it didn't help me. I think I have this issue because my logIn function needs more time to log in the user, and my handleLogIn function doesn't wait.
You've declared your router
within the ReactTree, so when the component rerenders for any reason, router
is redeclared and unmounts the old routing tree and mounts the new one, and this interrupts any active navigation actions.
Move the router
declaration out of the ReactTree.
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "/sign-in",
element: <LogIn />,
},
{
path: "/sign-up",
element: <Register />,
},
{
element: <PrivateRoute />,
children: [
{
path: "/",
element: <GroupMenu />,
children: [
{
path: "/add-group",
element: <AddGroupModal />,
},
],
},
{
path: "/group-content",
element: <GroupContent />,
},
{
path: "/dashboard",
element: <Dashboard />,
},
],
},
],
},
]);
const Main = () => {
return (
<React.StrictMode>
<ThemeProvider theme={theme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
);
};
Or memoize it so it can be provided as a stable reference.
const Main = () => {
const router = useMemo(createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "/sign-in",
element: <LogIn />,
},
{
path: "/sign-up",
element: <Register />,
},
{
element: <PrivateRoute />,
children: [
{
path: "/",
element: <GroupMenu />,
children: [
{
path: "/add-group",
element: <AddGroupModal />,
},
],
},
{
path: "/group-content",
element: <GroupContent />,
},
{
path: "/dashboard",
element: <Dashboard />,
},
],
},
],
},
]), []);
return (
<React.StrictMode>
<ThemeProvider theme={theme}>
<RouterProvider router={router} />
</ThemeProvider>
</React.StrictMode>
);
};
I recommend also converting your handleLogIn
callback to an async
function so you can await
the logIn
to resolve and manage the loading
state better, i.e. to set loading
false only after the log-in attempt has succeeded or failed.
const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
return setLogInError("email is not correct");
}
if (logInValues.password === "") {
return setLogInError("password field can not be empty");
}
try {
setLogInError("");
setLoading(true);
await logIn(logInValues.email, logInValues.password);
navigate("/");
} catch (errors) {
setLogInError("Failed to log-in in account");
} finally {
setLoading(false);
}
};
There still appears to be a bit of a synchronization issue between when logIn
, or auth.signInWithEmailAndPassword
, successfully authenticates a user and when Firebase can push an auth change to your onAuthStateChange
listener which updates the currentUser
state client-side.
Here's a couple additional things you can try:
Capture the userCredential.user
(see UserCredential) value from the resolved signInWithEmailAndPassword
call and update the currentUser
state.
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const navigate = useNavigate();
const [currentUser, setCurrentUser] = useState<firebase.User | undefined | null>(undefined);
...
const logIn = (email: string, password: string) => {
return auth.signInWithEmailAndPassword(email, password);
};
const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
return setLogInError("email is not correct");
}
if (logInValues.password === "") {
return setLogInError("password field can not be empty");
}
try {
setLogInError("");
setLoading(true);
const userCredential = await logIn(logInValues.email, logInValues.password);
setCurrentUser(userCredential.user);
navigate("/");
} catch (errors) {
setLogInError("Failed to log-in in account");
} finally {
setLoading(false);
}
};
...
Place the navigate
call to the back of the Javascript event queue. Note that this is a bit of a hack, but by delaying the navigate
call it allows Javascript to process any other additional enqueued callbacks to process first, then issue the navigation action.
const handleLogIn = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!/^[a-zA-Z\s].*@.*$/.test(logInValues.email)) {
return setLogInError("email is not correct");
}
if (logInValues.password === "") {
return setLogInError("password field can not be empty");
}
try {
setLogInError("");
setLoading(true);
await logIn(logInValues.email, logInValues.password);
setTimeout(() => navigate("/"));
} catch (errors) {
setLogInError("Failed to log-in in account");
} finally {
setLoading(false);
}
};