I am using Firestore and have access to state that will inform me if the user is authenticated or not. I am mapping these conditions to props and will use react-router-dom's redirect to navigate user to the login page if not authenticated.
However, when I console log the authentication status i see that initially it will show false while it loads this information and then switched to true. However, by the time it sees that the users is authenticated, I have already redirected them.
How am I suppose to redirect users based on their authentication within react?
export class PrimaryNavBar extends Component {
render(){
const { auth, location } = this.props;
const authenticated = auth.isLoaded && !auth.isEmpty;
return (
<React.Fragment>
{!authenticated ? <Redirect to="/"/> : null}
<Navbar bg="primary" variant="dark">
<Container>
<Link to="/">
....
I have looked at other answers here but they don't really helped with redirects, just render nulls if not authenticated.
Update
I restructured my code to redirects will happen in the app.js file
The new problem is, whenever I user changes route, it will recheck auth which will be false at first and they end up back at the login screen.
class App extends Component {
state = {
isAuthenticated: this.props.isAuthenticated
}
render() {
const logoutHandler = () => {
this.props.firebase.logout();
};
const authenticated = this.props.auth.isLoaded && !this.props.auth.isEmpty;
return (
<Aux>
{!authenticated ? <Redirect to="/" /> : null}
<Route path="/" exact component={Login} />
<Route path='/(.+)' render={() => (
<React.Fragment>
<PrimaryNavBar logout={logoutHandler} />
<Switch>
<Route path="/dashboard" exact component={Dashboard} />
<Route path="/auctions" component={Auctions} />
<Route path="/auctions/:id" component={AuctionItem} />
<Route path="/auctions/create-auction" component={CreateAuction} />
<Route path="/bidders/create-bidder" component={CreateBidder} />
<Route path="/bidders/:id" component={Bidder} />
<Route path="/bidders" component={Bidders} />
</Switch>
</React.Fragment>
)} />
<ReduxToastr position="bottom-right" />
</Aux>
);
}
}
const mapStateToProps = state => {
return {
isAuthenticated: state.firebase.auth.uid,
auth: state.firebase.auth
};
};
const mapDisptachToProps = dispatch => {
return {};
};
export default withRouter(withFirebase(
connect(
mapStateToProps,
mapDisptachToProps
)(App)
));
You should probably wait untill auth.isLoaded is true? If thats the case, you could just do
const { auth, location } = this.props;
if (!auth.isLoaded) return null
This will refrain your nav component from rendering untill the authentication info is loaded. You might want to consider to implement this check (and the redirect) in a parent component level, though.
Related
In my ReactJS app, routes are configured in below way:
class App extends React.Component{
constructor(props){
super(props);
this.state={
isLoggedin: false
}
}
componentDidMount(){
if(localStorage.getItem('name'))
{
this.setState({
isLoggedin: true
})}
}
render(){
return(
<>
<Switch>
<Route exact path="/" render={()=><Login isLoggedin={this.state.isLoggedin} />} />
<Route exact path="/login" render={<Login isLoggedin={this.state.isLoggedin} />} />
<Route exact path="/home" render={()=><Home isLoggedin={this.state.isLoggedin} />} />
</Switch></>
);
}
}
In Login.js:
class Login extends React.Component{
render(){
if(this.props.isLoggedin) return <Redirect to="/home" />;
return(
<h1>Login here</h1>
);
}
}
In Home.js:
class Home extends React.Component{
render(){
if(!this.props.isLoggedin) return <Redirect to="/login" />;
return(
<h1>Home</h1>
);
}
}
So what this code will do is that when the user visits the /, it would first go to Login component and as soon as isLoggedin is set to true, it would redirect the user to Home.js. Same thing would happen if user is not loggedin and he tries to access /home, he would be redirected to /login. Since I am using local storage, all of this would happen in flash of eye. It is also working just fine.
But I doubt if it is the best method to achieve my goal. I want to know if there is any more advisable method to do this.
Thanks!!
A more advisable method would be to decouple the auth checks from the components and abstract this into custom route components.
PrivateRoute - if the user is authenticated then a regular Route is rendered and the props are passed through, otherwise redirect to the "/login" path for user to authenticate.
const PrivateRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Route {...props} /> : <Redirect to="/login" />;
};
AnonymousRoute - Basically the inverse of the private route. If the user is already authenticated then redirect them to the "/home" path, otherwise render a route and pass the route props through.
const AnonymousRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Redirect to="/home" /> : <Route {...props} />;
};
From here you render the Login and Home components into their respective custom route components.
<Switch>
<PrivateRoute
isLoggedIn={this.state.isLoggedIn} // *
path="/home"
component={Home}
/>
<AnonymousRoute
isLoggedIn={this.state.isLoggedIn} // *
path={["/login", "/"]}
component={Login}
/>
</Switch>
* NOTE: The isLoggedIn={this.state.isLoggedIn} prop is only required here since the isLoggedIn state resides in the App component. A typical React application would store the auth state in a React Context or in global state like Redux, and thus wouldn't need to be explicitly passed via props, it could be accessed from within the custom route component.
Full sandbox code:
const PrivateRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Route {...props} /> : <Redirect to="/login" />;
};
const AnonymousRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Redirect to="/home" /> : <Route {...props} />;
};
class Login extends Component {
render() {
return (
<>
<h1>Login here</h1>
<button type="button" onClick={this.props.login}>
Log in
</button>
</>
);
}
}
class Home extends Component {
render() {
return (
<>
<h1>Home</h1>
<button type="button" onClick={this.props.logout}>
Log out
</button>
</>
);
}
}
export default class App extends Component {
state = {
isLoggedIn: false
};
componentDidMount() {
if (localStorage.getItem("isLoggedIn")) {
this.setState({
isLoggedIn: true
});
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.isLoggedIn !== this.state.isLoggedIn) {
localStorage.setItem("isLoggedIn", JSON.stringify(this.state.isLoggedIn));
}
}
logInHandler = () => this.setState({ isLoggedIn: true });
logOutHandler = () => this.setState({ isLoggedIn: false });
render() {
return (
<div className="App">
<div>Authenticated: {this.state.isLoggedIn ? "Yes" : "No"}</div>
<ul>
<li>
<Link to="/">/</Link>
</li>
<li>
<Link to="/home">home</Link>
</li>
<li>
<Link to="/login">log in</Link>
</li>
</ul>
<Switch>
<PrivateRoute
isLoggedIn={this.state.isLoggedIn}
path="/home"
render={() => <Home logout={this.logOutHandler} />}
/>
<AnonymousRoute
isLoggedIn={this.state.isLoggedIn}
path={["/login", "/"]}
render={() => <Login login={this.logInHandler} />}
/>
</Switch>
</div>
);
}
}
I have 3 components, WelcomePage, RegisterPage, HeaderLogOut.
I want that when user is on RegisterPage to hide that HeaderLogOut component. In RegisterPage user can click to logo icon of the app and will be redirected to WelcomePage and it will have HeaderLogOut component, which can redirect user back to RegisterPage.
I have seen similar questions and articles that show how this is done. But my problem is that for example, when I am on WelcomePage and I click to login, RegisterPage page has the HeaderLogOut and only after I refresh the page it disappears. Same with WelcomePage, when I am on RegistrationPage and I click the logo of the app, it redirects me to WelcomePage, and I have no HeaderLogOut, only after refresh it appears. I have had multiple attempts but I do not understand why it does not render the components immediately without a hard refresh.
For routing I use useHistory from react-router-dom hook.
const history = useHistory();
onClick={() => {
history.push("/welcome");
}}
What I have in App.js
return (
<React.Fragment>
{/* {history.location.pathname === "/register" ? null : currentUser ? (
<HeaderLogIn />
) : (
<HeaderLogOut />
)} */}
{location.pathname !== "/register" && !currentUser && <HeaderLogOut />}
{location.pathname !== "/register" && currentUser && <HeaderLogIn />}
<Switch>
<Route exact path="/register" component={RegistrationPage} />
<Route
exact
path="/welcome"
render={() => (currentUser ? <Redirect to="/" /> : <WelcomePage />)}
/>
... other routes
</Switch>
</React.Fragment>
);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
I play with my user reducer like this to test either a logged in user or not.
const user = {
username: "bvcbvc",
email: "fdfsdfs",
};
const INITIAL_STATE = {
currentUser: null, // user, if logged in
};
I think the issue is that the App component isn't rerendered so either the location or currentUser aren't re-evaluated.
A solution could be to render, conditionally, the header components on a generic route outside the Switch so the Route can pass updated location value for conditional testing.
return (
<React.Fragment>
<Route
render={({ location }) =>
location.pathname !== "/register" && currentUser ? (
<HeaderLogIn />
) : (
<HeaderLogOut />
)
}
/>
<Switch>
<Route exact path="/register" component={RegistrationPage} />
<Route
exact
path="/welcome"
render={() => (currentUser ? <Redirect to="/" /> : <WelcomePage />)}
/>
... other routes
</Switch>
</React.Fragment>
);
I have a login page which is a public route and a dashboard page which is a private route as follows:
App.js:
function App() {
return (
<Router>
<div className="App">
<Switch>
<PublicRoute path="/" exact component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Switch>
</div>
</Router>
);
}
And this is my private route component:
let store = require("store");
const PrivateRoute = ({component: Component, ...options}) => {
// const finalComponent = user != null && user.auth == true ? component : Login;
const [userData, setUserData] = useState({});
useEffect(() => {
setUserData(store.get("userData"));
}, []);
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Component {...props} />
:
<Redirect to="/"/>
}
/>
);
};
export default PrivateRoute;
Now the problem that I am facing is that if I open the dashboard localhost:9999/dashboard none of my components are visible because they are routed inside the dashboard component.
So when I open this localhost:9999/dashboard I want react to redirect to localhost:9999/dashboard/home and also show the components if the user is logged in.
I tried to do this :
return (
<Route
{...options}
render={(props) =>
store.get("userData") ?
<Redirect to="/dashboard/home" component={<Component {...props}} />
:
<Redirect to="/"/>
}
/>
);
This did route to the home page but nothing was visible at all. I mean the components inside dashboard weren't visible at all as well.
This is what I am getting. You see it redirects but doesn't show anything:
I believe that you could be more specific about which file is the code that you are showing.
But I feel that what you are trying to accomplish something like this:
PrivateRouter
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
const PrivateRoute = ({component: Component, restricted, ...rest}) => {
return (
<Route {...rest} render={props => (
user.auth == true ?
<Redirect to="/dashboard" />
: <Redirect to="/dashboard/home" />
)} />
);
};
export default PrivateRoute;
I am trying to build a role based access control React app.
My vision was that when the App mounts, it checks if user token exists. If it does, it runs checkAuthToken() and sets the state accordingly.
Where I am struggling is: redirection doesn't work as I expect it to.
Here is my code:
in App.js
function App() {
const { isAuthenticated, user } = useSelector(state => {
return state.userState;
});
const dispatch = useDispatch();
useEffect(() => {
checkAuthToken();
}, []);
return (
<Router>
<Switch>
<Route exact path='/'>
{!isAuthenticated ? (
<Redirect to='/login'/>
) : (
<Redirect to={`/${user.role}`} />
)}
</Route>
<Route
path="/login"
render={() => {
return <Login />;
}}
/>
<Route
path="/admin"
render={() => {
return <AdminDashboard user={user} />;
}}
/>
<Route
path="/staff"
render={() => {
return <OrderMenu user={user} />;
}}
/>
<Route component={ErrorPage} />
</Switch>
</Router>
);
}
export default App;
My understanding is React rerenders when state or props change. When I get the updated state from Redux, the app component should rerender and thus go through the isAuthenticated check and redirect accordingly, but i'm being proven wrong.
Currently, if there's no token, it redirects to Login route. After user logs in, token is set in localStorage. So to test it out, I close and open a new tab, try to go to path / expecting it to redirect me to /[admin|staff] route since checkAuthToken would successfully set the state but redirect doesn't work and just lands on /login. However, I could access /[admin|staff] if I type in manually.
PrivateRoute is rendering before Redux gets current user data from the server. What is the best way to fix this issue?
The component looks like below. userAuth.isAuthenticated eventually updates to true but it renders <Redirect to="/login" /> first before it gets updated.
const userAuth = {
isAuthenticated: false
}
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
// PROBLEM: this renders before Redux gets user authentication data from the server
userAuth.isAuthenticated ? <Component {...props} /> : <Redirect to="/login" />
)}/>
)
class App extends Component {
componentDidMount() {
// Get current user data when user refresh the browser
// This sets isLoginSuccess: true is current user is logged-in in Redux store
this.props.getCurrentUserSession();
}
render() {
// Show loading icon while fetching current user data
if(this.props.user.isFetchingCurrentUserSession){
return (<div><img src={squareLogo} className="logo-loading" alt="Loading icon" /></div>);
}
// Set current user authentication status
userAuth.isAuthenticated = this.props.user.isLoginSuccess
return (
<Router>
<div>
<Route path="/public" component={Public} />
<PrivateRoute path="/private" component={Private} />
</div>
</Router>
);
}
}
function mapStateToProps(state) {
return state
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
getCurrentUserSession
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
You can do like this:
1.Intialize isAuthenticated to null
2.in render return use conditional rendering of the private component
const userAuth = {
isAuthenticated: null
}
in App render return:
return (
<Router>
<div>
<Route path="/public" component={Public} />
{(userAuth.isAuthenticated!==null)?<PrivateRoute path="/private"
component={Private} />:null}
</div>
</Router>
);