I have used react-router-dom v6 and google firebase authentication. In react-router dom v5, I have added, it is working well, and in v6 I have changed some from the v6 documentation. I couldn't find the wrong.
useFirebase.js :
import { useEffect, useState } from "react";
import initializeFirebase from "./../Pages/Authentication/firebase/firebase.init";
import {
getAuth,
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
onAuthStateChanged,
updateProfile,
signOut,
} from "firebase/auth";
initializeFirebase();
const useFirebase = () => {
const [user, setUser] = useState({});
const [userData, setUserData] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [optStatus, setOtpStatus] = useState("");
const [registrationStatus, setRegistrationStatus] = useState(false);
const [authError, setAuthError] = useState("");
const [admin, setAdmin] = useState(false);
const [assitantAdmin, setAssistantAdmin] = useState(false);
const [applicant, setApplicant] = useState(false);
const auth = getAuth();
const defaultOTP = "123456";
const registerUser = (
email,
password,
name,
phone,
dateOfBirth,
institutionId,
employeeId,
employeeType,
institution,
requestedRole,
role
) => {
setIsLoading(true);
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
setAuthError("");
const newUser = {
email,
displayName: name,
photoURL: "",
phoneNumber: phone,
dateOfBirth,
institutionId,
employeeId,
employeeType,
institution,
requestedRole,
role,
};
setUser(newUser);
//save user to the database
saveUser(
email,
name,
"",
phone,
dateOfBirth,
institutionId,
employeeId,
employeeType,
institution,
requestedRole,
role,
"POST"
);
// send name to firebase after register with email and password
updateProfile(auth.currentUser, {
displayName: name,
})
.then(() => {})
.catch((error) => {});
})
.catch((error) => {
setAuthError(error.message);
})
.finally(() => setIsLoading(false));
};
// sign is with email and password
const loginUserWithEmailPassword = (email, password, navigate) => {
setIsLoading(true);
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
// const destination = location?.state?.from || "/";
navigate("/home");
setAuthError("");
})
.catch((error) => {
setAuthError(error.message);
})
.finally(() => setIsLoading(false));
};
//observe user state
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
} else {
setUser({});
setUserData({});
}
});
setIsLoading(false);
return () => unsubscribe;
}, []);
//check and set admin
useEffect(() => {
fetch(`http://localhost:5000/users/${user.email}`)
.then((res) => res.json())
.then((data) => {
setUserData(data.userData);
setAdmin(data.admin);
setAssistantAdmin(data.assitantAdmin);
setApplicant(data.applicant);
});
}, [user.email]);
// save users into database
const saveUser = (
email,
displayName,
photoURL,
phoneNumber,
dateOfBirth,
institutionId,
employeeId,
employeeType,
institution,
requestedRole,
role,
method
) => {
const user = {
email,
displayName,
photoURL,
phoneNumber,
dateOfBirth,
institutionId,
employeeId,
employeeType,
institution,
requestedRole,
role,
};
if (method === "POST" || method === "PUT" || method === "DELETE") {
fetch("http://localhost:5000/users", {
method: method,
headers: {
"content-type": "application/json",
},
body: JSON.stringify(user),
}).then();
} else {
fetch("http://localhost:5000/users", {
method: method,
headers: {
"content-type": "application/json",
},
}).then();
}
};
// log out user
const logOut = () => {
signOut(auth)
.then(() => {
// Sign-out successful.
})
.catch((error) => {})
.finally(() => setIsLoading(false));
};
return {
user,
userData,
admin,
assitantAdmin,
applicant,
authError,
isLoading,
registrationStatus,
defaultOTP,
optStatus,
setIsLoading,
setOtpStatus,
setRegistrationStatus,
registerUser,
loginUserWithEmailPassword,
logOut,
};
};
export default useFirebase;
App.js :
<AuthProvider>
<Router>
<Routes>
<Route
path='/dashboard/*'
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
<Route path='/login' element={<Login />} />
</Routes>
</Router>
</AuthProvider>
PrivateRoute.js :
// this is private route
const PrivateRoute = ({ children }) => {
let { user, isLoading } = useAuth();
let location = useLocation();
if (isLoading) {
return <Spinner className='my-5' animation='border' variant='success' />;
}
if (user?.email) {
return children;
}
return (
<Navigate
to={{
pathname: "/login",
state: { from: location },
}}
/>
);
};
export default PrivateRoute;
Login.js :
const alertStyle = {
justifyContent: "center",
marginLeft: "auto",
marginRight: "auto",
};
user log in page:
const Login = () => {
const [loginData, setLoginData] = useState({});
const { user, loginUserWithEmailPassword, isLoading, authError } = useAuth();
let location = useLocation();
const navigate = useNavigate();
const handleOnBlur = (e) => {
const field = e.target.name;
const value = e.target.value;
const newLoginData = { ...loginData };
newLoginData[field] = value;
setLoginData(newLoginData);
};
//handle log in using email password
const handleLogin = (e) => {
loginUserWithEmailPassword(
loginData.email,
loginData.password,
location,
navigate
);
e.preventDefault();
};
return (
<div>
<Navigation />
<div className='container'>
<p className='mt-5'>
<b>Log In for Admin</b>
</p>
<div className='mb-5'>
{/* register form */}
{!isLoading && (
<form onSubmit={handleLogin}>
<br />
<div className='mb-3'>
<input
type='email'
name='email'
onBlur={handleOnBlur}
style={{ width: "300px" }}
className='form-control mx-auto'
id='exampleFormControlInput1'
placeholder='Your Email'
/>
</div>
<div className='mb-3'>
<input
type='password'
name='password'
onBlur={handleOnBlur}
style={{ width: "300px" }}
className='form-control mx-auto'
id='exampleFormControlInput1'
placeholder='Your Password'
/>
</div>
<button
type='submit'
style={{ width: "300px" }}
className='btn btn-success mx-auto'
>
Login
</button>
</form>
)}
{/* Spinner */}
{isLoading && (
<Spinner className='my-5' animation='border' variant='success' />
)}
<p className='mt-3'>
<b>New User? </b>
<NavLink to='/signup' className='text-decoration-none'>
<b>Register & Submit Application</b>
</NavLink>
</p>
</div>
{/* success and error alert */}
{user?.email && (
<Alert severity='success' style={alertStyle}>
Account created successfully!
</Alert>
)}
{authError && (
<Alert severity='error' style={alertStyle}>
{authError}
</Alert>
)}
</div>
</div>
);
};
export default Login;
CodePudding user response:
Issue
I think the issue is that the initial isLoading state is false:
const [isLoading, setIsLoading] = useState(false);
and the initial user state is truthy:
const [user, setUser] = useState({});
When reloading the page after authenticating the loading state is never triggered:
//observe user state
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
} else {
setUser({});
setUserData({});
}
});
setIsLoading(false);
return () => unsubscribe;
}, []);
In PrivateRoute the isLoading check fails because it is false, and so the user.email property is checked. Because the user object isn't populated yet the redirect occurs.
const PrivateRoute = ({ children }) => {
let { user, isLoading } = useAuth();
let location = useLocation();
if (isLoading) { // <-- (1) false, so no spinner
return <Spinner className='my-5' animation='border' variant='success' />;
}
if (user?.email) { // <-- (2) undefined, so no children
return children;
}
return (
<Navigate // <-- (3) navigate
to={{
pathname: "/login",
state: { from: location },
}}
/>
);
};
Solution
I believe the solution is trivial, start with an initially loading private route component.
const [isLoading, setIsLoading] = useState(true);
The allows the mounting auth check to work for your UI instead of against it.
//observe user state
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
} else {
setUser({});
setUserData({});
}
});
setIsLoading(false); // <-- (2) done initializing, isLoading false
return () => unsubscribe;
}, []); // <-- (1) mounting useEffect, start isLoading true
So now the PrivateRoute component will receive an isLoading true on the initial render cycle.
