I'm trying to get some auth experience and I've got React with React Router, I found a custom auth check for routes that I thought looked good, and tried to implement. Basically it would sign the user in, change the auth value to true and be able to call on that auth value from the hook to check.
Here's my codesandbox, my problem is I have to use AWS Cognito for this project so the sign in call has to be from an async function and not through a promise...
Clicking 'sign in check' calls handleLogin and starts the async function in the useAuthHook with signIn, which sets authed to true via an effect hook. That change is reflected by using the 'auth check' button, but when trying to navigate to a protected route the console logs the default values.
Here's the steps;
...
<Button variant="primary" onClick={handleLogin} type="button">
sign in check
</Button>
...
const handleLogin = () => {
signIn();
};
Now the signIn hook from useAuthHook;
async signIn() {
const user = "totally real"; //AWS await request
testValue = "a diff string";
if (user) {
setUser(user);
console.log(authed, user);
return "/storageSolution";
}
}
the effect hook that updates a ref hook and a useState hook(testing both cases);
React.useEffect(() => {
if (user) {
authed.current = true;
setStateAuth(true);
}
}, [user]);
both stateAuth and the authed ref are returned along with the earlier signIn, and used in RequireAuth before my routes;
export function RequireAuth({ children }) {
const location = useLocation();
const { authed, user, stateAuth } = useAuth();
console.log(authed, user, stateAuth);
return authed === true ? (
children
) : (
<Navigate to="/" replace state={{ path: location.pathname }} />
);
}
but clicking protected route check after signing in shows default values in the console, whereas clicking auth check shows the updates values.
I found https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function this on stale state, but neither of the reasons it gives seem to be the problem. I've got several different ways of updating and reading that value, but none work. What am I missing?
React hooks don't share state. Move all the state and logic from the useAuth hook into the AuthProvider component. After this is done the useAuth hook simply returns the current authContext value.
Example:
const authContext = React.createContext();
export function useAuth() {
return React.useContext(authContext);
}
export function AuthProvider({ children }) {
const authed = React.useRef(false);
const [user, setUser] = React.useState();
const [stateAuth, setStateAuth] = React.useState(false);
let testValue = "some string";
React.useEffect(() => {
if (user) {
authed.current = true;
setStateAuth(true);
}
}, [user]);
return (
<authContext.Provider
value={{
authed,
user,
testValue,
stateAuth,
async signIn() {
const user = "totally real"; // where the await auth req would be
testValue = "a diff string";
if (user) {
setUser(user);
console.log(authed, user);
return "/storageSolution";
}
}
}}
>
{children}
</authContext.Provider>
);
}
index.js
Import and wrap the app with the AuthProvider component.
...
import { RequireAuth, AuthProvider } from "./useAuthHook";
...
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<LoginSignup />} />
<Route
path="/storageSolution"
element={
<RequireAuth>
<StorageSolution />
</RequireAuth>
}
/>
<Route path="/secret" element={<SecretRoute />} />
</Routes>
</BrowserRouter>
</AuthProvider>
</React.StrictMode>,
rootElement
);
Related
I am having some issues with my routing currently when authenticated. Whenever I try to access my ViewPortfolio page at localhost:3000/portfolio/portfolioId it will redirect me back to my homepage. I am not sure what is going on. I have also tried manipulating the URL by modifying it to the correct URL link but it also redirects me back to /homepage when I am authenticated. The source codes can be found below. App.js is my router with PrivateRoute as the private route component and finally, CreateInterview.js where I redirect using js windows.location function to ViewPortfolio.js which will use useParams() react hook to get the param. But instead now after creating successfully and redirect to the correct URL with the portfolioId it will redirect back to homepage within less than a second.
PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useAuth()
return (
<Route
{...rest}
render={(props) => {
if (currentUser) {
return <Component {...props} />
} else {
return <Redirect to={{
pathname: "/",
state:{
from: props.location
}
}}/>
}
}
}>
</Route>
)
}
export default PrivateRoute
App.js
import React from "react"
.
.
.
import PublicRoute from "./PublicRoute";
function App() {
return (
<AuthProvider>
<Router>
<Switch>
{/* Auth Routes */}
<PublicRoute exact path='/' component={Login} />
.
.
.
<PrivateRoute exact path='/createInterview' component={CreateInterview} />
<PrivateRoute path='/manageInterview' component={ManageInterview} />
<PrivateRoute path='/portfolio/:portfolioId' component={ViewPortfolio} />
{/* Non-Existance Routes */}
<Route path="*" component={() => "404 NOT FOUND"} />
</Switch>
</Router>
</AuthProvider>
)
}
export default App
CreatInterview.js redirecting in js (onSubmit of the form)
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
}).then(function(docRef) {
console.log("This is the Document ID " + docRef.id.toString());
console.log(docRef.id);
window.location = '/portfolio/' + docRef.id;
})
setLoading(false)
}
Part of ViewPortfolio.js to receive the portfolioId from CreateInterview.js
const ViewPortfolio = () => {
let { portfolioId } = useParams();
AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth, database } from "../firebase";
import { getDocs, query, where } from "firebase/firestore";
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
const Doc = query(database.usersRef, where("email", "==", email));
getDocs(Doc).then((querySnapshot) => {
let values = '';
querySnapshot.forEach((doc) => {
values = doc.id;
});
var userUpdate = database.usersRef.doc(values);
userUpdate.update({
lastActive: new Date().toLocaleString('en-SG'),
})
})
});
}
function logout() {
return auth.signOut();
}
function forgetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
function updateDisplayName(name) {
return currentUser.updateDisplayName(name)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged( user => {
setLoading(false)
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
forgetPassword,
logout,
updateEmail,
updatePassword,
updateDisplayName,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
The initial currentUser state matches the unauthenticated state, so when the app initially renders, if you are accessing a protected route the redirection will occur because the currentUser state hasn't updated yet.
Since onAuthStateChanged returns null for unauthenticated users then I suggest using anything other than null for the initial currentUser state. undefined is a good indeterminant value. You can use this indeterminant value to conditionally render a loading indicator, or nothing at all, while the auth status is confirmed on the initial render.
AuthProvider
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(); // <-- undefined
...
PrivateRoute
const PrivateRoute = (props) => {
const { currentUser } = useAuth();
if (currentUser === undefined) {
return null; // or loading spinner, etc...
}
return currentUser
? (
<Route {...props} />
)
: (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
You should also really replace the window.location = '/portfolio/' + docRef.id; logic with a history.push('/portfolio/' + docRef.id); so you are not unnecessarily reloading the page.
const history = useHistory();
...
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const docRef = await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
});
history.push('/portfolio/' + docRef.id);
} catch (error) {
// handle error, clear loading state
setLoading(false);
}
}
I am having some issues with my routing currently when authenticated. Whenever I try to access my ViewPortfolio page at localhost:3000/portfolio/portfolioId it will redirect me back to my homepage. I am not sure what is going on. I have also tried manipulating the URL by modifying it to the correct URL link but it also redirects me back to /homepage when I am authenticated. The source codes can be found below. App.js is my router with PrivateRoute as the private route component and finally, CreateInterview.js where I redirect using js windows.location function to ViewPortfolio.js which will use useParams() react hook to get the param. But instead now after creating successfully and redirect to the correct URL with the portfolioId it will redirect back to homepage within less than a second.
PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useAuth()
return (
<Route
{...rest}
render={(props) => {
if (currentUser) {
return <Component {...props} />
} else {
return <Redirect to={{
pathname: "/",
state:{
from: props.location
}
}}/>
}
}
}>
</Route>
)
}
export default PrivateRoute
App.js
import React from "react"
.
.
.
import PublicRoute from "./PublicRoute";
function App() {
return (
<AuthProvider>
<Router>
<Switch>
{/* Auth Routes */}
<PublicRoute exact path='/' component={Login} />
.
.
.
<PrivateRoute exact path='/createInterview' component={CreateInterview} />
<PrivateRoute path='/manageInterview' component={ManageInterview} />
<PrivateRoute path='/portfolio/:portfolioId' component={ViewPortfolio} />
{/* Non-Existance Routes */}
<Route path="*" component={() => "404 NOT FOUND"} />
</Switch>
</Router>
</AuthProvider>
)
}
export default App
CreatInterview.js redirecting in js (onSubmit of the form)
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
}).then(function(docRef) {
console.log("This is the Document ID " + docRef.id.toString());
console.log(docRef.id);
window.location = '/portfolio/' + docRef.id;
})
setLoading(false)
}
Part of ViewPortfolio.js to receive the portfolioId from CreateInterview.js
const ViewPortfolio = () => {
let { portfolioId } = useParams();
AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth, database } from "../firebase";
import { getDocs, query, where } from "firebase/firestore";
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
const Doc = query(database.usersRef, where("email", "==", email));
getDocs(Doc).then((querySnapshot) => {
let values = '';
querySnapshot.forEach((doc) => {
values = doc.id;
});
var userUpdate = database.usersRef.doc(values);
userUpdate.update({
lastActive: new Date().toLocaleString('en-SG'),
})
})
});
}
function logout() {
return auth.signOut();
}
function forgetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
function updateDisplayName(name) {
return currentUser.updateDisplayName(name)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged( user => {
setLoading(false)
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
forgetPassword,
logout,
updateEmail,
updatePassword,
updateDisplayName,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
The initial currentUser state matches the unauthenticated state, so when the app initially renders, if you are accessing a protected route the redirection will occur because the currentUser state hasn't updated yet.
Since onAuthStateChanged returns null for unauthenticated users then I suggest using anything other than null for the initial currentUser state. undefined is a good indeterminant value. You can use this indeterminant value to conditionally render a loading indicator, or nothing at all, while the auth status is confirmed on the initial render.
AuthProvider
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(); // <-- undefined
...
PrivateRoute
const PrivateRoute = (props) => {
const { currentUser } = useAuth();
if (currentUser === undefined) {
return null; // or loading spinner, etc...
}
return currentUser
? (
<Route {...props} />
)
: (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
You should also really replace the window.location = '/portfolio/' + docRef.id; logic with a history.push('/portfolio/' + docRef.id); so you are not unnecessarily reloading the page.
const history = useHistory();
...
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const docRef = await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
});
history.push('/portfolio/' + docRef.id);
} catch (error) {
// handle error, clear loading state
setLoading(false);
}
}
I want to redirect to <Chats /> component after the user has signed in through their gmail ID. I also want the uid and userName to be send as props to the component, but I'm not able to redirect to that. I've tried <Navigate /> inside the promise, I've tried window.location.replace('') too. <Navigate /> seems to do nothing and window.location.replace('') redirects but the props are empty, I have no idea what is happening, can anyone help?
Here is the code:
import React, {useState} from 'react'
import { signInWithPopup, signOut } from 'firebase/auth'
import { auth, db, provider } from './firebase-config'
import { set, ref, onValue } from 'firebase/database'
import { BrowserRouter as Router, Routes, Route, Navigate} from 'react-router-dom'
import Chats from './components/Chats'
import Homepage from './components/Homepage'
const App = () => {
const [uid, setUid] = useState('')
const [userName, setUserName ] = useState('')
const [redirect, setRedirect] = useState(false)
const signInWithGoogle = () => {
signInWithPopup(auth, provider)
.then(result => {
setUid(result.user.uid)
setUserName(result.user.displayName)
setRedirect(true)
}).catch(err => console.log(err))
}
const signOutWithGoogle = () => {
signOut(auth, provider)
setUserName('')
setRedirect(false)
}
redirect && (<Navigate to='/chats' />)
return (
<Router>
<Routes>
<Route path="/" element={<Homepage signInWithGoogle={signInWithGoogle} signOutWithGoogle={signOutWithGoogle} />} />
<Route path="/chats" element={<Chats uid={uid} name={userName}/>} />
</Routes>
</Router>
)
}
export default App
you can put both homepage and chats under the same url path and render the page that meets the requirements
<Routes>
<Route path="/" element={uid || userName ? <Chats uid={uid} name={userName}/> :<Homepage signInWithGoogle={signInWithGoogle} signOutWithGoogle={signOutWithGoogle} />} />
</Routes>
or
<Routes>
{uid || userName ?
<Route path="/chats" element={<Chats uid={uid} name={userName}/>} />
:
<Route path="/" element={<Homepage signInWithGoogle={signInWithGoogle} signOutWithGoogle={signOutWithGoogle} />} />
}
</Routes>
Issues
You can't return JSX from a callback and expect it to be rendered into the DOM and do anything.
Don't use the window.location method as this will reload the page, thus remounting your app, so any React state will be lost if it wasn't persisted to localStorage or similar.
Solution 1
Use the useNavigate hook to access the navigate function and issue an imperative redirect with the appropriate data sent along in route state. In order for the App component to use the useNavigate hook though, you will need to lift the Router above it in the ReactTree.
Example:
import { Routes, Route, useNavigate } from 'react-router-dom';
const App = () => {
const navigate = useNavigate();
const signInWithGoogle = () => {
signInWithPopup(auth, provider)
.then(result => {
const { displayName, uid } = result.user.uid;
navigate(
"/chats",
{
replace: true,
state: { uid, userName: displayName },
}
);
})
.catch(err => console.log(err));
};
const signOutWithGoogle = () => {
signOut(auth, provider);
setUserName('');
setRedirect(false);
};
return (
<Routes>
<Route path="/" element={<Homepage signInWithGoogle={signInWithGoogle} signOutWithGoogle={signOutWithGoogle} />} />
<Route path="/chats" element={<Chats />} />
</Routes>
);
};
index.js or wherever App is rendered
import { BrowserRouter as Router } from 'react-router-dom';
...
<Router>
<App />
</Router>
Chat
Use the useLocation hook to access the passed route state values.
const { state } = useLocation();
...
const { uid, userName } = state || {};
Solution 2
If you don't want to send the data in route state you could try using the local uid and userName state and still use the navigate function to imperatively redirect. You'll still need to lift the Router up the ReactTree.
Example:
const App = () => {
const navigate = useNavigate();
const [uid, setUid] = useState('');
const [userName, setUserName ] = useState('');
const signInWithGoogle = () => {
signInWithPopup(auth, provider)
.then(result => {
setUid(result.user.uid);
setUserName(result.user.displayName);
navigate("/chats", { replace: true });
})
.catch(err => console.log(err));
};
const signOutWithGoogle = () => {
signOut(auth, provider);
setUserName('');
setRedirect(false);
};
return (
<Routes>
<Route path="/" element={<Homepage signInWithGoogle={signInWithGoogle} signOutWithGoogle={signOutWithGoogle} />} />
<Route path="/chats" element={<Chats uid={uid} name={userName}/>} />
</Routes>
)
};
In Firebase, there's one function that can help you to implement some code when the authentication change (i.e.: after logging in using an email, after signing up, after logging out, ...), which is:
onAuthStateChanged(auth: Auth, nextOrObserver: NextOrObserver<User>, error?: ErrorFn, completed?: CompleteFn): Unsubscribe;
Where:
auth: The Auth Instance
nextOrObserver: a callback triggered on authentication change
error: a callback triggered on error
completed (optional): a callback triggered when observer is removed
You can read more at: https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged
Basically, when using this function, you are subscribing to the authentication observer, and this function returns an unsubscribe function. Hence, this is can be perfectly used with useEffect in React. After seeing your problem, you can put these lines into the App component:
useEffect(() => {
const unsubscribe = onAuthStateChanged(user => {
// When no user is logged in and the path is '/chats' => redirect to login page
if(user === null && window.location.href.includes('/chats')) {
if(window.location.href.includes('/chats')) return; // This is important, if this line is not presented, the page will reload infinitely
window.location.href = '/'; // Redirect to '/'
}
// After user is logged in => redirect to '/chats'
if(user !== null && window.location.href.includes('/')) {
if(window.location.href.includes('/')) return;
window.location.href = '/chats'; // Redirect to '/chats'
}
});
return unsubcribe; // This must also included
})
When seeing your code, I see that you put all authentication methods and states in App component. I recommend you to create a React context that contains those lines of code (and also the above code), since they should be global.
I have a userContext like this :
import React, { useContext, useEffect, useState } from "react";
import * as userService from "./services/appService";
const UserContext = React.createContext();
export function useUserContext() {
return useContext(UserContext);
}
export function UserProvider({ children }) {
const [user, setUser] = useState();
useEffect(() => {
const fetchData = async () => {
const { data } = await userService.allDetails();
setUser(data);
};
fetchData();
}, []);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
}
I'm wrapping routes with User Provider like this
<UserProvider>
<Route path="/signup" component={Signup} />\
<Route path="/login" component={Login} />\
<ProtectedRoute path="/resetpassword" component={Reset} />
<ProtectedRoute path="/settings" component={Settings} />
<UserProvider>
I'm trying to access it inside a functional component like this:
function Settings(){
const user = useUserContext();
const id = user.id
useEffect(() => {
console.log(id)
},[])
return (
.......
)
}
I'm getting user as undefined in Settings component.
In the UserProvider component, you defined a state user with initial value of undefined, because you did not pass any value to useState. Even though you are making an API call to update the state of user state, but keep in mind that fetching is an asynchronous operation and may take time to finish, which is why when you try to access user context in Settings component, the value is still undefined. You could add an if statement check here to see if user context is truthy or not and use it if it's truthy only, which means that the fetch finished and user state has been updated
Since the user is only loaded as an Effect after the first render of UserProvider, it is still undefined during the first render.
I suggest you suspend rendering the UserProvider and its contents until the fetch completed:
export function UserProvider({ children }) {
const [user, setUser] = useState();
useEffect(() => {
userService.allDetails()
.then(({ data }) => setUser(data));
}, []);
return !user ? null : (
<UserContext.Provider value={user}>
{children}
</UserContext.Provider>
);
}
I am trying to implement private routing to my react web application. I have a login, a sign app and a home page, and i want the user to access the homepage only if he is logged in or signed in. I am using Firebase authentication.
I know that my private route component is wrong but because i am new i don't know how to fix it.
I get the error that currentuser is undefined.
Thank you!
PrivateRoute.js
export default function PrivateRoute({ children }) {
const currentUser = useAuth()
console.log(currentUser.email)
return currentUser ?(
<Navigate to="/home" />
) : (
<Navigate to="/signin" />
);
App.js
function App() {
return (
<Routes>
<Route restricted={true} path='/signin' element={<SignInSide/>}/>
<Route
path="/home"
element={
<PrivateRoute>
<Home/>
</PrivateRoute>
}
/>
<Route path='/signup' element={<SignUpside/>}/>
</Routes>
useAuth function in firebase.js
// Initialize Firebase
export const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export function useAuth() {
const [ currentUser, setCurrentUser ] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, user => setCurrentUser(user));
return unsub;
}, [])
return currentUser;
}
Your PrivateRoute component does not make sense, since it is not using the children prop. You should either redirect to the signin page when you don't have a user, or display the children, when you have a user, isn't it ?
It could look like this :
export const PrivateRoute = ({ children }) => {
const currentUser = useAuth()
return currentUser ? children : <Navigate to="/signin" />;
};
However, your error doesn't come from the router, but rather from your useAuth hook it seems. Initially, your user is undefined, so it is normal that you get an error saying this.
If you want to logout the email of the user, try to do the following instead:
if(currentUser)
console.log(currentUser.email)
This way, it will not be undefined.
Furthermore, there is no restricted prop in the Route component of react-router, in the v6.
I hope it solves your issue, otherwise please provide a reproducible example, so it is easier to locate the issue you are facing.
I actually found the problem.
i needed to use context provider and then wrap the app with it
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [pending, setPending] = useState(true);
useEffect(() => {
auth.onAuthStateChanged((user) => {
setCurrentUser(user)
setPending(false)
});
}, []);
if(pending){
return <>Loading...</>
}
return (
<AuthContext.Provider
value={{
currentUser
}}
>
{children}
</AuthContext.Provider>
);
};