I have a firebase onAuthStateChange and a set of private routes to be rendered with react router v6
useEffect(() => {
const fetchData = async () =>
await auth.onAuthStateChanged(async authUser => {
console.log('here in authuser')
if (authUser) {
await dispatch(setUser('SET_USER', authUser))
} else {
await dispatch(setUser('SET_USER', null))
}
})
fetchData()
}, [])
<Route path='/' element={<PrivateRoute user={users} />}>
<Route path='/courses/:id' element={<CourseDetails />} />
<Route
path='/'
element={<Courses emailId={users?.user?.email} />}
/>
<Route path='/enrolled' element={<Enrolled />} />
<Route path='/authored' element={<Authored />} />
<Route path='/users' element={<Users />} />
</Route>
In the protected route component I am checking if user is null then redirect user to login page else render the children.
if (user === null || user.user === null) {
console.log('Entered!!!!!')
return <Navigate to='/login' replace />
} else {
return children ? children : <Outlet />
}
On page refresh if I am logged in also I am redirected to login route because onauthstatechange has not finished executing. So user is null inside the
What is the right way to handle this situation and make sure that user gets navigated to the same page where reload happened.
You can create a component that handles the check for you.
So in your route you wrap your PrivateRoute with your route component like this :
<Route path='/courses/:id' element={<PrivateRoute><CourseDetails /></PrivateRoute> } />
So what this component does is check if the user is authed. If it is authed it will render the child component that is your route. If not it will redirect you to /login
they are talking about it in the docs
const PrivateRoute =({ children }: { children: JSX.Element }) => {
let auth = useAuth();
let location = useLocation();
if (!auth.user) {
// Redirect them to the /login page, but save the current location they were
// trying to go to when they were redirected. This allows us to send them
// along to that page after they login, which is a nicer user experience
// than dropping them off on the home page.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
Related
Sorry if the question is badly phrased. I am working with React.js and Parse Server at school. The issue is the following:
I would like to have protected routes, just for users that are currently logged in, and I am trying to do it by using the .getSessionToken() Parse built-in function. I retrieve the token successfully, get access to the protected routes only if I am logged in, but as soon as I refresh the page, the route is inaccessible again because the token is updated.
I have a working solution, but it creates some issues at the moment. The issue is that even if I have access on the page, there's the following error in the console: No routes matched location "/page1"
Do you have any idea of how I can prevent the token from updating or any other more elegant solution for this issue?
My code is the following:
1.When logging in, I pass a setter using useState() through props, all the way to the App.js where the routes are. The only reason I set the token here is in order to navigate to the next page which is protected.
await Parse.User.logIn(username, password);
const currentUser = Parse.User.current();
const currentToken = currentUser.getSessionToken();
setSessionToken(currentToken);
navigate("/page1");
2.Here, I am checking if a user is currently in my local storage, and if yes, get the token associated with the user.
//sessionToken = the one I pass with props from step 1
const [sessionToken, setSessionToken] = useState();
//This makes sure that I get the token again and again if page is refreshed
const [refreshedToken, setRefreshedToken] = useState();
const authenticateUser = async () => {
if (Parse.User.current() !== null) {
const token = Parse.User.current().getSessionToken();
setRefreshedToken(token);
}
};
useEffect(async () => {
await authenticateUser();
}, []);
3.Since I have 2 state variables now, I make a conditional rendering by using both.
<Route
path="/login"
element={<LoginPage setSessionToken={setSessionToken} />}
/>
{(sessionToken !== undefined || refreshedToken !== undefined) && (
<Fragment>
<Route path="/page1" element={<Page1 />} />
<Route path="/page2" element={<Page2 />} />
<Route path="/page3" element={<Page3 />} />
I believe the issue here is that you are attempting to navigate to a path "/page1" before the component rendering the routes knows it should be rendering them, hence the No routes matched location "/page1" error.
You may want to create an AuthWrapper component that checked the sessionStorage and token and renders an Outlet for nested routes you want to protect, otherwise redirect the users to another page, i.e. to home or login.
import { Outlet, Navigate, useLocation } from 'react-router-dom';
const AuthWrapper = (props) => {
const location = useLocation();
... business logic to check sesstionStorage/token ...
return (sessionToken !== undefined || refreshedToken !== undefined)
? <Outlet />
: <Navigate to="/login" replace state={{ from: location }} />
};
Now declare the routes, wrapping the routes you want to protect in the AuthWrapper layout component.
<Route
path="/login"
element={<LoginPage setSessionToken={setSessionToken} />}
/>
<Route element={<AuthWrapper /* pass any session props here */ />}>
<Route path="/page1" element={<Page1 />} />
<Route path="/page2" element={<Page2 />} />
<Route path="/page3" element={<Page3 />} />
</Route>
This is my router:
const Router = () => {
const { user } = useContext(UserContext)
return (
<Switch>
{user.token ?
<>
<Route exact path="/" component={Wall}/>
<Route path="/profile" component={Profile}/>
<Route path="/discover" component={Discover}/>
<Route path="/settings" component={Settings}/>
<Route path="/post" component={Post}/>
<Route path="/deleteuser" component={DeleteUser}/>
<Route path="/profileimg" component={ProfileImg}/>
</> :
<>
<Route exact path="/" component={Splash}/>
<Route path="/auth" component={Auth}/>
<Route path="/create" component={Create}/>
<Route path="/forgot" component={Forgot}/>
</>
}
{!user.token && <Redirect to="/"/>}
<Route component={Notfound}/>
</Switch>
)
}
As you can see I have protected routs based on whether the user has a token in context.
In my app, I have a function logout() that clears local storage and state information which of course includes the user token.
My problem is I can't seem to find any documentation on the best way to redirect to "/" if the user is on a protected route and this logout() function is called.
To be clear what I want is this: logout() => <Redirect to="/"/> because there is no longer a token in state.
I have thought of multiple ways of doing this BUT I'm looking for the best or cleanest solution:
An approach using history - history.location.pathname === "/protected" && !user.token && <Redirect to="/"/> I'm sure there is a way to achive the same logic this with <Redirect/> in my router?
An approach using redirect in the JSX of every single protected component - {!user.token && <Redirect to="/"/>} I would rather not put this in the component JSX of every single protected route I have!
I could pass the history prop to the logout() function and do this - history && history.push("/") I would also rather not do this as I would need to pass the history prop every time I call logout() which feels rather messy.
You can either have a route <Route exact path="/logout" component={Logout}/>.
with
export default const Logout = () => {
useEffect(() => {
//remove token here
}, []);
return <Redirect to="/">
}
or a function logout with transition to your home like
const history = useHistory();
const logout = () => {
// remove tokens
history.push('/');
}
or both
const logout = () => {
// remove tokens
return <Redirect to="/"/>
}
I didn't do anything inside router.js or logout() in the end. I instead used an approach that obviously had escaped my mind at the time of this post.
In order to redirect with React, we need a way to trigger a rerender. I'm can't be sure if all of the methods discussed above do in fact rerender however the default way of achieving this in a React is to update Context or your store state if you're using Redux. Then based on that state, in a component you know is always going to be rendered, add some code to redirect.
This is my code in Nav:
const Nav = ({ history }) => {
const { user, setUser } = useContext(UserContext)
useEffect(() => { // Redirect if user.redirect === truthy.
user.redirect && history.push(user.redirect)
setUser({ ...user, redirect: false })
}, [user.redirect]) // eslint-disable-line react-hooks/exhaustive-deps
*** rest of the code in Nav ***
}
I have a react + firebase application which has protected routes. I face issue if a logged in user accesses the login page. The issue is that the login page gets displayed for a second and then redirects to the home page i.e protected route. I feel the issue is because the value retrieved from context in the login page to check if user is authenticated gets updated after the route is resolved. Can someone give me pointers on how should I fix this. Ideally I would not want the user to see the login page for sometime if the user is already authenticated.
//App.js
render() {
return (
<AuthProvider>
<Router>
<Switch>
<PrivateRoute exact path="/" component={Home}></PrivateRoute>
<Route exact path="/login" component={LoginPage}></Route>
<Route exact path="/signup" component={SignUp}></Route>
</Switch>
</Router>
</AuthProvider>
);
}
}
//AuthProvider
import React, { useEffect, useState } from "react"
import { fire } from "./Fire"
export const AuthContext = React.createContext();
//this component will maintain the current user throughout the app
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
//empty array as second arg to useEffect hook as we only want to trigger it once
useEffect(() => {
console.log("useEffect")
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>{children}</AuthContext.Provider>
)
}
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
} />
)
}
//login
render() {
if (this.props.context.currentUser)
return <Redirect to="/" />
return (
<Login email={this.state.email} password={this.state.password} inputHandler={this.onInputChange} loginHandler={this.onLoginClick} />
)
}
You should probably add a loading check in your PrivateRoute file which will just show a Loading... or a loader if the user is not loaded yet.
For that you will have to do some minor changes in your AuthProvider file and in PrivateRoute.
//PrivateRoute
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
//useContext hook makes it very easy to retrieve the value
const { currentUser } = useContext(AuthContext)
return (
<Route {...rest} render={
routeProps => {
console.log("currentUser" + currentUser);
return !!currentUser ? (
<RouteComponent {...routeProps} />
) : currentUser === 'loading' ? <h1>Loading...</h1>
:(
<Redirect to={"/login"} />
)
}
} />
)
}
I'm developing a basic react application and included react-router.
I have a simple authentication control with the Local Storage.
After a user inputs username and password and clicks login, I do an HTTP call and take response from the server with Axios. Then I set the localStorage 'user' item.
To protect a route I implemented the PrivateRoute component where I check if 'user' in localStorage is set.
I already tried to move set Local Storage inside then() in the Axios HTTP call but nothing changed.
Api CALL
loginUser (username,password) {
return HTTP.post('/login', null, { params: {
username,
password
}})
Api.loginUser(username,password)
.then( (response) => {
console.log("Response DATA");
Api.saveUserData(response.data);
this.setState({ redirect: true });
})
RENDER METHOD
if (this.state.redirect === true) {
return <Redirect to='/home'/>;
}
APP COMPONENT
class App extends Component {
render() {
return (
<Router>
<Route path="/login" component={Login} />
<PrivateRoute path="/home" component={Home} />
</Router>
);
}
}
PRIVATE ROUTE COMPONENT
const PrivateRoute = ({ component: Component, ...rest }) => {
const isLoggedIn = AuthService.isAuthenticated();
return (
<Route
{...rest}
render={props =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
The problems seem to be: the local storage is set after the redirect because is null. So I get blank page instead of loading the Home Component. If i refresh the page, the code works fine.
I'm having some problems with implementing PrivateRoute in React. Here is my code:
class App extends Component {
constructor(props) {
super(props);
this.state = {
currentUser: null,
loadingUser: true
}
}
componentDidMount() {
this.onAuth();
};
onAuth = () => {
getCurrentUser().then((json) => {
console.log(json);
this.setState({
currentUser: json,
loadingUser: false
})
}).catch((error) => {
this.setState({
currentUser: null,
loadingUser: false,
})
})
};
logout = () => {
logout();
this.setState({
currentUser: null,
loadingUser: false
});
this.props.history.push("/");
toast.info("Succesfully logout.");
};
render() {
return (
<div className="body">
<ToastContainer closeOnClick={false}/>
<ApplicationHeader currentUser={this.state.currentUser} logout={this.logout}/>
<Grid>
<div className="app-content">
<Switch>
<Route exact path="/vote/:id" render={(props) => <Vote currentUser={this.state.currentUser} {...props}/>}/>
<Route exact path="/login" render={() => <Login onAuth={this.onAuth} />}/>
<PrivateRoute authed={this.state.currentUser != null} exact path="/vote" component={NewProcess} />
<PrivateRoute authed={this.state.currentUser != null} exact path="/items" component={NewItems} />
<Route component={NotFound}/>
</Switch>
</div>
</Grid>
<Footer/>
</div>
);
}
}
const PrivateRoute = ({component: Component, authed, ...rest}) => {
return (
<Route
{...rest}
render={(props) => authed === true
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}} />} />
)
}
When user posts credentials (or App main component gets rendered) onAuth method gets invoked and sets (or not) currentUser property of App's state. This property is null (when user is not authenticated) and represents userdetails such like id and username (when user is authenticated). Then, in PrivateRoute based on that property component gets rendered or application redirects user back to the login page. And that doesn't work well. I mean when i'm already authenticated and try to access any of private route, i am redirected to proper component. Problem occurs in 2 situations:
right after logging in - application doesnt redirect me to component
i want to access, insted i stay on the login page.
refreshing page (in browser) corresponded to private route.
It seems like PrivateRoute component doesnt get refreshed when currentUser property gets changed, which is kinda weird because i'm using similar approach in ApplicationHeader to display username when user is authenticated (and that is refreshed correctly).
So, what am i doing wrong here?
I did it rendering the route or the redirect depending on the condition and it worked for me.
Something like the following:
class PrivateRouteComponent extends React.Component{
render(){
return (
this.props.isAuthenticated===true?
(
<Route path={this.props.path} render={this.props.component} />
):
(<Redirect to={{
pathname: '/login',
state: { from: this.props.path }
}} />));
}
}