My main App component keeps track of the user that is currently logged in via the firebase onAuthStateChanged callback, which I can then use to redirect the user to the /login route if the user object is null. This works fine, but if you navigate to a different route while on the login page, you don't get redirected back, which causes errors as other routes require you to be logged in to function properly. Here is the code:
export function App() {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<BrowserRouter>
<Switch>
<Route path="/login" exact component={LoginPage}/>
<Route path="/" exact component={HomePage}/>
{!user ? <Redirect to="/login"/> : null}
</Switch>
</BrowserRouter>
);
}
I've tried moving !user ? <Redirect to="/login"/> to the top of the Switch component, but that just makes it so you log out every time the page is refreshed. Any ideas on how to solve this? Thanks.
Why not recompose your Route element to have private routers and public routes? Private routes will be those requiring authentication and public once will not require it. When someone tries to access a private route without authentication, they will automatically be sent away.
Create an element called PrivateRoute and put your firebase auth inside it. Example:
const PrivateRoute = ({children, ...props}) => {
const auth = firebase.auth();
const [user, setUser] = useState(null);
useEffect(()=>{
auth.onAuthStateChanged(()=> {
setUser(auth.currentUser);
})
}, []);
return (
<Route {...props} render={() => {
return valid === null ?
<div>Some kind of loader/spinner here...</div>
:
user ?
children
:
<Redirect to='/login' />
}} />
)
}
Then in your App, use it like so:
return (
<BrowserRouter>
<Switch>
<PrivateRoute exact path="/">
<HomePage />
</PrivateRoute>
<Route exact path="/login" component={LoginPage} />
</Switch>
</BrowserRouter>
);
This will redirect anybody trying to access / to /login if they are not authenticated.
Later any route you create can be wrapped like this if it requires authentication.
I am using the following approach and it works fine (just copy my existing project that works):
import React, {useState, useEffect} from 'react'
import {BrowserRouter as Router, Switch, Route, Redirect} from "react-router-dom"
import {connect} from "react-redux"
import useAuth from './hooks/useAuth'
import styles from './styles.js'
import Landing from './components/Landing'
import Login from './components/Login'
import Logout from './components/Logout'
import Post from './components/Post'
import Users from './components/Users'
import User from './components/User'
import Signup from './components/Signup'
import Profile from './components/Profile'
import AddSocieta from './components/AddSocieta'
import Constructor from './components/Constructor'
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [dimension, setDimention] = useState({windowWidth: 0, windowHeight: 0})
const currentStyles = {
...styles,
showFooterMenuText: styles.showFooterMenuText(dimension),
showSidebar: styles.showSidebar(dimension),
topMenuCollapsed: styles.topMenuCollapsed(dimension),
topMenuHeight: styles.topMenuHeight(dimension),
paddingLeftRight: styles.paddingLeftRight(dimension),
fullScreenMenuFontSize: styles.fullScreenMenuFontSize(dimension),
showSubLogoText: styles.showSubLogoText(dimension),
roundedImageSize: styles.roundedImageSize(dimension)
};
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
updateDimensions();
window.addEventListener("resize", updateDimensions);
return function cleanup() {
window.removeEventListener("resize", updateDimensions);
}
}, [auth, profile]);
function updateDimensions() {
let windowWidth = typeof window !== "undefined"
? window.innerWidth
: 0;
let windowHeight = typeof window !== "undefined"
? window.innerHeight
: 0;
setDimention({windowWidth, windowHeight});
}
return (<Router>
<Redirect to="/app/gare"/>
<div className="App">
<Switch>
<Route path="/constructor"><Constructor styles={currentStyles}/></Route>
<Route path="/post"><Post/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/users"><Users styles={currentStyles}/></Route>
<Route path="/user/:uid"><User styles={currentStyles}/></Route>
<Route path="/app"><Landing styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/gare"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
<Route path="/add-societa"><AddSocieta styles={currentStyles}/></Route>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;
Related
I'm using Firebase v9 and react-router v6. I haven't used v6 so this was quite confusing. How can I make it where the guest user can only access the login page. Only users who were logged in can access the homepage and other pages.
Everytime I'll reload any page, it will show this in the console but it will still direct the user to the right page :
No routes matched location "/location of the page"
How can I use a private route for the profile page?
//custom hook
export function useAuth() {
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user));
return unsub;
}, []);
return currentUser;
}
App.js
import { auth, useAuth } from "./Firebase/utils";
import { onAuthStateChanged } from "firebase/auth";
function App() {
const currentUser = useAuth();
const user = auth.currentUser;
const navigate = useNavigate();
console.log(currentUser?.email);
useEffect(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
console.log(uid);
navigate("/Home");
// ...
} else {
// User is signed out
// ...
navigate("/");
}
});
}, []);
return (
<div>
<div>
<Routes>
{currentUser ? (
<>
//If i do it this way and I'll go the profile page and reload it, it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={
<LogInPage />
}
/>
</>
)}
</Routes>
</div>
</div>
);
}
export default App;
This is what the console.log(user) shows:
Package.json file:
Issues
The main issue is that the currentUser value is initially falsey
const [currentUser, setCurrentUser] = useState();
and you are making a navigation decision on unconfirmed authentication status in App
<Routes>
{currentUser ? (
<>
// If i do it this way and I'll go the profile page and reload it,
// it will always go to back to the Homepage.
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</>
) : (
<>
<Route
path="/"
element={<LogInPage />}
/>
</>
)}
</Routes>
When refreshing the page the currentUser state is reset, is undefined, i.e. falsey, and only the "/" path is rendered.
Solution
In react-router-dom is a common practice to abstract route protection into a specialized "protected route" component. You will also want to conditionally handle the indeterminant state until your Firebase auth check has had a chance to confirm an authentication status and update the currentUser state.
Example:
export function useAuth() {
const [currentUser, setCurrentUser] = useState(); // <-- initially undefined
useEffect(() => {
const unsub = onAuthStateChanged(auth, (user) => setCurrentUser(user)); // <-- null or user object
return unsub;
}, []);
return { currentUser };
}
AuthWrapper - Uses the useAuth hook to check the authentication status of user. If currentUser is undefined it conditionally returns early null or some other loading indicator. Once the currentUser state is populated/defined the component conditionally renders either an Outlet for nested/wrapped Route components you want to protect, or the Navigate component to redirect to your auth route.
import { Navigate, Outlet, useLocation } from 'react-router-dom';
const AuthWrapper = () => {
const location = useLocation();
const { currentUser } = useAuth();
if (currentUser === undefined) return null; // <-- or loading spinner, etc...
return currentUser
? <Outlet />
: <Navigate to="/" replace state={{ from: location }} />;
};
App - Unconditionally renders all routes, wrapping the Home and Profile routes in the AuthWrapper layout route.
function App() {
return (
<div>
<div>
<Routes>
<Route element={<AuthWrapper />}>
<Route path="/Home" element={<Home />} />
<Route path="/Profile" element={<ProfilePage />} />
</Route>
<Route path="/" element={<LogInPage />} />
</Routes>
</div>
</div>
);
}
I am trying to send users to different routes based on the roles of the user which is stored in the realtime firebase database, but I am getting the following error:
App.js:36 Uncaught (in promise) TypeError: Cannot read properties of null (reading 'users')
Following is my App.js file where I am making the call for the firebase data"
App.js
`
import React from "react";
import { Route, Routes, Navigate } from "react-router-dom";
import Landing from "./components/Landing";
import PhoneDetails from "./components/PhoneDetails";
import Home from "./components/Home/App.jsx";
import Signup from "./components/Signup";
import SignIn from "./components/Signin";
import { auth } from "./firebase-config.js";
import { useEffect } from "react";
import FirebaseData from "./firebaseData";
function App() {
document.body.style = "background: #F8F5FA;";
// getting the user data from firebase
const firebaseData = FirebaseData();
const [displayName, setDisplayName] = React.useState("");
const [isAuthenticated, setIsAuthenticated] = React.useState(false);
const [role, setRole] = React.useState("");
useEffect(() => {
auth.onAuthStateChanged((user) => {
if (user) {
// User is signed in
// ...
setIsAuthenticated(trEue);
setDisplayName(user.displayName);
// ERROR ON THIS LINE
setRole(firebaseData.users[user.uid].role)
// setRole(firebaseData.users[user.uid].role);
} else {
// User is signed out
// ...
setIsAuthenticated(false);
setDisplayName("");
setRole("");
}
});
}, []);
console.log("role:", role);
return (
<Routes>
<Route
path="/"
exact
element={
<Home isAuthenticated={isAuthenticated} displayName={displayName} role={role}/>
}
/>
<Route path="/signup" element={<Signup />} />
<Route path="/signin" element={<SignIn />} />
{
isAuthenticated && role === "admin" ? (
<Route path="/home" element={<Landing />} />
) : (
<Route
path="/"
element={
<Home isAuthenticated={isAuthenticated} displayName={displayName} />
}
/>
)
}
{isAuthenticated && role === "admin" ? (
<Route path="/details" element={<PhoneDetails />} />
) : (
<Route
path="/"
element={
<Home isAuthenticated={isAuthenticated} displayName={displayName} />
}
/>
)}
<Route path="/" element={<Navigate replace to="/" />} />
<Route path="*" element={<Navigate replace to="/" />} />
</Routes>
);
}
export default App;
`
In my App.js I am calling the FirebaseData() file which is given below:
firebaseData.js
`
import {database} from "./firebase-config";
import React from "react";
import {ref, onValue} from "firebase/database";
import {useEffect} from "react";
const db = database;
export default function FirebaseData() {
const [data, setData] = React.useState(null);
useEffect(() => {
onValue(ref(db), (snapshot) => {
setData(snapshot.val());
});
}, []);
return data;
}
`
The data in the firebase DB is stored in the following format:
users
---->uid
------>roles
I've tried to find the solution for this but couldn't find any. Any help will be appreciated!
check firebaseData is defined in the useEffect and check if users exists using ? operator
useEffect(() => {
if(firebaseData){
auth.onAuthStateChanged((user) => {
if (user) {
// User is signed in
// ...
setIsAuthenticated(trEue);
setDisplayName(user.displayName);
// ERROR ON THIS LINE
setRole(firebaseData.users?.[user.uid]?.role)
// setRole(firebaseData.users[user.uid].role);
} else {
// User is signed out
// ...
setIsAuthenticated(false);
setDisplayName("");
setRole("");
}
});
}
}, [firebaseData]);
today I'm having an issue where react loads the route before my API verifies that the user's JWT token is valid. When using EJS I could pass in a middleware to the route and the middleware would not contain the next() parameter. As a result the server wouldn't render the EJS which is exactly what I want to achieve with react. Also is it possible to make useNavigate not reload when navigating the that specific route?
My routes in App.js currently look like this:
<Route element={<ProtectedRoute access={access}></ProtectedRoute>}>
<Route
path="/login"
exact
element={<Login login={login} access={access}></Login>}
></Route>
<Route
path="/signup"
exact
element={<Signup signup={signup} access={access}></Signup>}
></Route>
<Route
path="/forgot-password"
exact
element={<ForgotPassword access={access}></ForgotPassword>}
></Route>
<Route
path="/reset-password"
exact
element={<ResetPassword access={access}></ResetPassword>}
></Route>
</Route>;
The access function looks like this:
const access = async (token) => {
return await axios.post(
"http://localhost:5000/access",
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
};
The protected route component looks like this:
import { useState, useContext } from "react";
import { useLocation, useNavigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(false);
const { auth } = useContext(AuthContext);
const navigate = useNavigate();
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
if (authorized) {
navigate('/');
} else {
return <Outlet></Outlet>;
}
};
export default ProtectedRoute;
When I use this code my login component renders a bit before the code navigates back to the home page, how do I make the login component not render at all and just make it stay on the home page?
Issue
The ProtectedRoute component's initial authorized state masks the confirmed unauthenticated state, and since the component doesn't wait for authentication confirmation it happily and incorrectly redirects to "/".
The ProtectedRoute component incorrectly issues a navigation action as an unintentional side-effect via the navigate function and doesn't return valid JSX in the unauthenticated case. Use the Navigate component instead.
If the user is authorized the ProtectedRoute should render the Outlet for a protected route to be rendered into, and only redirect to login if unauthorized.
Solution
The ProtectedRoute component should use an indeterminant initial authorized state that doesn't match either the authenticated or unauthenticated state, and wait for the auth status to be confirmed before rendering either the Outlet or Navigate components.
Example:
import { useState, useContext } from "react";
import { useLocation, Navigate, Outlet } from "react-router-dom";
import AuthContext from "../Context/AuthProvider";
const ProtectedRoute = ({ access }) => {
const location = useLocation();
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />;
};
Move the login route outside the ProtectedRoute layout route.
<Routes>
<Route
path="/login"
element={<Login login={login} access={access} />}
/>
<Route
path="/signup"
element={<Signup signup={signup} access={access} />}
/>
<Route
path="/forgot-password"
element={<ForgotPassword access={access} />}
/>
<Route
path="/reset-password"
element={<ResetPassword access={access} />}
/>
... other unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected routes ...
</Route>
</Routes>
To protect the login/signup/forgot/reset/etc routes
Create an AnonymousRoute component that inverts the Outlet and Navigate components on the authentication status. This time authenticated users get redirected off the route.
const AnonymousRoute = ({ access }) => {
const [authorized, setAuthorized] = useState(); // initially undefined!
const { auth } = useContext(AuthContext);
useEffect(() => {
const authorize = async () => {
try {
await access(auth.accessToken);
setAuthorized(true);
} catch (err) {
setAuthorized(false);
}
};
authorize();
}, []);
if (authorized === undefined) {
return null; // or loading indicator/spinner/etc
}
return authorized
? <Navigate to="/" replace />
: <Outlet />;
};
...
<Routes>
<Route element={<AnonymousRoute access={access} />}>
<Route path="/login" element={<Login login={login} access={access} />} />
<Route path="/signup" element={<Signup signup={signup} access={access} />} />
<Route path="/forgot-password" element={<ForgotPassword access={access} />} />
<Route path="/reset-password" element={<ResetPassword access={access} />} />
... other protected anonymous routes ...
</Route>
... unprotected routes ...
<Route element={<ProtectedRoute access={access} />}>
... other protected authenticated routes ...
</Route>
</Routes>
Apologies for this seemingly repated question but it is really biting me. I am trying to use the new ReactRouter v6 private routes and I think the best practice for me would be to make a call to the server to make sure the token is valid and has not been revoked. I am being badly beatean by an infinite loop entering the private route with the typical error
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
my private route looks like this:
import React, {useCallback, useEffect, useState} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
const [auth, setAuth] = useState(false)
const checkAuth = useCallback(() => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
setAuth(isAuthorized)
))
}, [])
useEffect(() => {
checkAuth()
}, [checkAuth])
return auth ? <Outlet /> : <Navigate to="/login" />;
}
export default PrivateRoute
and my routes are:
function App() {
return (
<HashRouter>
<Routes>
<Route exact path="/login" element={<LoginPage />} />
<Route exact path="/register" element={<RegisterPage />} />
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
</Routes>
</HashRouter>
)
}
export default App
I changed the PrivateRoute component to this:
import React, {useEffect, useRef} from "react"
import {Outlet, Navigate} from "react-router-dom"
import {Auth} from "../../api/authApi"
const PrivateRoute = () => {
let auth = useRef(false)
useEffect(() => {
const checkAuth = () => {
let authApi = new Auth()
authApi.isAuth().then(isAuthorized => (
auth.current = isAuthorized
))
}
checkAuth()
}, [])
return (auth.current ? <Outlet /> : <Navigate to="/login" />)
}
export default PrivateRoute
but still have the same issue. Is it something I am missing?
I tried it and found two issues:
The initial state auth is false so it will navigate to /login and unmount PrivateRoute at the first time, and then the component PrivateRoute(unmounted) got api response, I think maybe that's why you got warning. (I made an solution at the bottom)
The Route and Outlet components are used in the wrong way.
<Route path="/" element={
<PrivateRoute><HomePage /></PrivateRoute>
} />
should be modified to
<Route element={<PrivateRoute />}>
<Route path="/" element={<Home />} />
</Route>
The Code Sample :
I am trying to implement in my app.js a simple Protect Route Middleware.
If the user is authenticated, he can not go to "/login" and "/". I also made a simple function in app.js, which is checking if the user is not authenticated. If he is not authenticated, he will be redirected from "/home" to "/login". Unfortunately my website does not stop to refresh if I am doing that.
For example : If am not authenticated and I trying to visit "/home", I will be redirect to "/login", but then go in refresh loop ! :(
app.js
function App() {
const authenticated = useSelector((state) => state.user.authenticated)
const dispatch = useDispatch()
useEffect(() => {
if (!authenticated) {
window.location.href = '/login'
}
}, [
// I tried every possible combination
])
return (
<div>
<Router>
<Switch>
<Route exact path='/home' component={HomePage} />
<ProtectedRoute exact path='/login' component={LoginPage} />
<ProtectedRoute exact path='/' component={LoginPage} />
</Switch>
</Router>
</div>
)
}
ProtectedToute.js
import React from 'react'
import { useSelector } from 'react-redux'
import { Route, Redirect } from 'react-router-dom'
const ProtectedRoute = ({ component: Component, ...rest }) => {
const authenticated = useSelector((state) => state.user.authenticated)
console.log('Route', authenticated)
return (
<Route
{...rest}
render={(props) => {
if (authenticated) {
return <Redirect to='/home' />
} else {
return (
<Component {...props} />
)
}
}}
/>
)
}
export default ProtectedRoute