I'm getting started with ReactJS and I wanted to restrict the sign-in page for not authorized users only and I want to redirect those users to the page they were before after they log in (that part is not working as well).
The problem is that when I log in and then go back to /sign-in page, it says that I'm still not authorized, even tho I can literally see the access token in local storage (look at the picture below). However, if I refresh the page, NotAuthorizedRoute works and it redirects me back to home page.
How can I fix this in React? How can I also redirect back to the previously opened page and not / only?
In other words, const isLoggedIn = localStorage.getItem('access_token') !== null; is being activated only after a page refresh. In angular, I used to fix such things by #Input/#Output and EventEmitter but I'm new to React and I don't know how to deal with it yet.
Image:
Login.js
import React, { Fragment, useState } from 'react';
import { useHistory } from 'react-router-dom';
import UserService from '../../Services/UserService';
import {
makeStyles,
Typography,
Container,
TextField,
FormControlLabel,
Checkbox,
Button,
Grid,
CircularProgress
} from '#material-ui/core';
const useStyles = makeStyles(theme => ({
content: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(8, 0, 6)
},
form: {
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
}
}));
const Login = () => {
const classes = useStyles();
const history = useHistory();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const user = {
username,
password,
};
setLoading(true);
UserService.loginUser(user)
.then(res => {
localStorage.setItem('access_token', res.token);
setLoading(false);
history.push('/');
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}
return (
<Fragment>
<div className={classes.content}>
<Container maxWidth="sm">
<Typography component="h1" variant="h2" align="center" color="textPrimary" gutterBottom>
Sign In
</Typography>
<Typography variant="h5" align="center" color="textSecondary" paragraph>
If you are not registered, you should sign up.
</Typography>
</Container>
</div>
<Container maxWidth="md">
<Grid container justify="center" spacing={3}>
<Grid item xs={6}>
{!UserService.isLoggedIn ?
<form className={classes.form} noValidate onSubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="text"
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
error={error !== ''}
helperText={error}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={loading}
>
Sign In
</Button>
</form>
:
<Grid container justify="center">
Logged in.
</Grid>
}
{loading &&
<Grid container justify="center">
<CircularProgress className={classes.spinner} />
</Grid>
}
</Grid>
</Grid>
</Container>
</Fragment>
);
}
export default Login;
App.js
import React, { Fragment } from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import NotAuthorizedRoute from './Helpers/NotAuthorizedRoute';
import Navbar from './Components/Navbar/Navbar';
import Home from './Components/Home/Home';
import User from './Components/User/User';
import Login from './Components/Login/Login';
function App() {
return (
<Fragment>
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/users" component={User} />
<NotAuthorizedRoute path="/sign-in" component={Login} />
<Redirect from="*" to="/" />
</Switch>
</Router>
</Fragment>
);
}
export default App;
Helpers/NotAuthorizedRoute.js
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
Service/UserService.js
const isLoggedIn = localStorage.getItem('access_token') !== null;
const loginUser = async (user) => {
const { username, password } = user;
if (username === 'qwe' && password === '123') {
return { token: 'access_token' };
} else {
throw new Error('Wrong username or password');
}
}
export default { isLoggedIn, loginUser };
EDIT: If I put !localStorage.getItem('access_token') instead of !UserService.isLoggedIn in NotAuthorizedRoute, it works. Why?
In React, the component only re-renders when props or state changes. In your NotAuthorizedRoute you're directly using isLoggedIn param from file. Instead, you should pass isLoggedIn as prop from parent component. So you can re-write NotAuthorizedRoute.js as:
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, isLoggedIn=false, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
and call this component as
<NotAuthorizedRoute ... isLoggedIn={UserService.isLoggedIn} />
Related
I'm trying to make a private route from my login and signup pages to my dashboard page, but all the tutorials and guides that I've stumbled upon all require some sorta AuthContext thing and I didn't implement my authentication procedure using AuthContext.
I've tried different ways but none of them work and just end up giving me a blank page when I get to the dashboard page, what can I do to make it a private route? Using Firebase v9 btw.
SignUp.js
import './Signup.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { auth }from '../../../firebase';
import { createUserWithEmailAndPassword, onAuthStateChanged } from 'firebase/auth';
import { Typography, Container, TextField, Button, Alert } from '#mui/material';
const Signup = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [user, setUser] = useState({});
const history = useHistory();
onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
})
const signup = async (e) => {
e.preventDefault();
if (password !== confirmPassword) {
return setError("Passwords do not match")
}
try {
const user = await createUserWithEmailAndPassword(
auth,
email,
password
);
history.push("/dashboard/canvas");
} catch (err) {
setError(err.message);
}
}
return (
<>
<div className="text-div">
<Typography textAlign="center" variant="h3">Create a new account</Typography>
</div>
<Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
{ error && <Alert severity="error">{error}</Alert> }
<TextField label="Email" margin="dense" type="email" onChange={ (e) => {
setEmail(e.target.value);
}}/>
<TextField label="Password" margin="dense" type="password" onChange={ (e) => {
setPassword(e.target.value);
}}/>
<TextField label="Confirm Password" margin="dense" type="password" onChange={ (e) => {
setConfirmPassword(e.target.value);
}}/>
<Button onClick={signup} variant="contained" sx={{ marginTop: 2, }}>Sign Up</Button>
<div>
Already have an account? <Link to="/login" style={{ color: '#000' }}>Log In</Link>
</div>
</Container>
</>
)
}
export default Signup;
Login.js
import './Login.css'
import React, { useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { auth }from '../../../firebase';
import { Typography, Container, TextField, Button, Alert } from '#mui/material';
const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const history = useHistory();
const login = async () => {
try {
const user = await signInWithEmailAndPassword(
auth,
email,
password
);
//alert("Success, user is recognized");
history.push("/dashboard/canvas");
} catch (err) {
setError("The email or password you entered is incorrect");
}
}
return (
<>
<div className="text-div">
<Typography textAlign="center" variant="h3">Login</Typography>
</div>
<Container className="cont" maxWidth="xl" sx={{ backgroundColor: "#ffffff", width: 500, height: "auto", borderRadius: 4, marginTop: 5, display: "flex", flexDirection: "column", padding: 5, }}>
{ error && <Alert severity="error">{error}</Alert> }
<TextField label="Email" margin="dense" type="email" onChange={(e) => {
setEmail(e.target.value);
}}/>
<TextField label="Password" margin="dense" type="password" onChange={(e) => {
setPassword(e.target.value);
}}/>
<Button onClick={login} variant="contained" sx={{ marginTop: 2, }}>Login</Button>
<div>
Don't have an account? <Link to="/signup" style={{ color: '#000' }}>Create one here</Link>
</div>
<div>
<Link to="/request-password-reset" style={{ color: '#000' }}>Forgot your password?</Link>
</div>
</Container>
</>
)
}
export default Login;
firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
apiKey,
authDomain,
projectId,
storageBucket,
messagingSenderId,
appId,
measurementId
}
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
export {
auth
};
App.js
import './App.css';
import Home from './components/Pages/Home';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import Signup from './components/Pages/Signup/Signup';
import Login from './components/Pages/Login/Login';
import UserDashboard from './components/Pages/UserDashboard/UserDashboard';
import ForgotPassword from './components/Pages/Forgot-Password';
function App() {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/signup" component={Signup}/>
<Route path="/login" component={Login}/>
<Route path="/dashboard" component={UserDashboard}/>
<Route path="/request-password-reset" component={ForgotPassword}/>
</Switch>
</div>
</Router>
);
}
export default App;
If you are trying to create a private route component without persisting the authentication state somewhere in your app and exposed out via a React context then you will need to check the auth status asynchronously on each route change. This means you'll also need a "loading" or "pending" state while the auth status check occurring.
Here's an example implementation of just a custom private route sans any persisted state.
import { useEffect, useState } from 'react';
import { Route, Redirect } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';
const PrivateRoute = props => {
const [pending, setPending] = useState(true);
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsubscribe = onAuthStateChanged(
auth,
user => {
setCurrentUser(user);
setPending(false);
},
error => {
// any error logging, etc...
setPending(false);
}
);
return unsubscribe; // <-- clean up subscription
}, []);
if (pending) return null; // don't do anything yet
return currentUser
? <Route {...props} /> // <-- render route and component
: <Redirect to="/login" />; // <-- redirect to log in
};
react-router-dom#6
Custom route components are out in v6, use a layout route. The PrivateRoute component will replace Route with Outlet for nested routes to render their matched element prop into, and Navigate replaces Redirect.
import { useEffect, useState } from 'react';
import { Outlet, Navigate } from 'react-router-dom'; // v4/5
import { onAuthStateChanged } from 'firebase/auth';
import { auth }from '../../../firebase';
const PrivateRoute = props => {
const [pending, setPending] = useState(true);
const [currentUser, setCurrentUser] = useState();
useEffect(() => {
const unsubscribe = onAuthStateChanged(
auth,
user => {
setCurrentUser(user);
setPending(false);
},
error => {
// any error logging, etc...
setPending(false);
}
);
return unsubscribe; // <-- clean up subscription
}, []);
if (pending) return null; // don't do anything yet
return currentUser
? <Outlet /> // <-- render outlet for routes
: <Navigate to="/login" replace />; // <-- redirect to log in
};
Wrap the routes you want to protect.
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/request-password-reset" element={<ForgotPassword />} />
<Route element={<PrivateRoute />}>
<Route path="/dashboard" element={<UserDashboard />} />
</Route>
</Routes>
</Router>
);
}
need help making private route with react redux and firebase
the issue is:
when i click on sign in the user is sign it correctly and the state is updated on redux store but the user is not redirected to dashboard but typing "/dashboard" URL works after clicking the button signing in also trying to go "/dashboard" when the user state user null works
App.js
import React, { useEffect } from "react";
import { connect, Provider } from "react-redux";
import store from "./store";
import { setUser, clearUser } from "./actions/userActions";
import {
BrowserRouter as Router,
Route,
Switch,
withRouter,
} from "react-router-dom";
import Login from "./components/auth/Login";
import Register from "./components/auth/Register";
import Home from "./components/pages/Home";
import Dashboard from "./components/pages/Dashboard";
import firebase from "./firebase";
import PrivateRoute from "./components/PrivateRoute";
function App({ setUser, clearUser, currentUser, history, isLoading }) {
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
clearUser();
}
});
return () => {
unsubscribe();
clearUser();
};
}, [clearUser, history, setUser]);
return (
<Switch>
<Route path="/" component={Home} exact />
<PrivateRoute
path="/dashboard"
component={Dashboard}
currentUser={currentUser}
isLoading={isLoading}
/>
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</Switch>
);
}
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
const AppWithRouter = withRouter(
connect(mapStateToProps, { setUser, clearUser })(App)
);
const AppWithAuth = () => (
<Provider store={store}>
<Router>
<AppWithRouter />
</Router>
</Provider>
);
export default AppWithAuth;
PrivateRoute.js
import React from "react";
import { Redirect, Route } from "react-router-dom";
const PrivateRoute = ({
component: Component,
currentUser,
isLoading,
...rest
}) => {
return isLoading ? (
<h1>Spinner</h1>
) : (
<Route
{...rest}
render={(props) =>
currentUser ? <Component {...props} /> : <Redirect to="login" />
}
/>
);
};
export default PrivateRoute;
Login.js
import React, { useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
//firebase
import firebase from "../../firebase";
import {
Grid,
Box,
TextField,
CssBaseline,
Avatar,
Container,
Typography,
LinearProgress,
Button,
makeStyles,
} from "#material-ui/core";
const Login = (props) => {
const [form, setForm] = useState({
email: "",
password: "",
});
const [errors, setErrors] = useState([]);
const [loading, setLoading] = useState(false);
const handleChange = (event) =>
setForm({ ...form, [event.target.name]: event.target.value });
const handleSubmit = (e) => {
e.preventDefault();
if (!form.email || !form.password) {
return setErrors(["Fill in all fields"]);
}
setErrors([]);
setLoading(true);
firebase
.auth()
.signInWithEmailAndPassword(form.email, form.password)
.then((signedInUser) => {
setLoading(false);
props.history.push("dashboard");
})
.catch((err) => {
setLoading(false);
setErrors([err.message]);
});
};
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center",
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}></Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate onsubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={handleChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
onChange={handleChange}
/>
<Box color="red">
{errors.length > 0 && errors.map((err, i) => <span>{err}</span>)}
</Box>
<Grid justify="center"></Grid>
{loading ? <LinearProgress size={14} /> : ""}
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={handleSubmit}
>
Sign in
</Button>
<Grid container>
<Grid item>
<Link to="/register" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
<Box mt={8}></Box>
</Container>
);
};
const mapStateToProps = (state) => ({
isLoading: state.user.isLoading,
currentUser: state.user.currentUser,
});
export default connect(mapStateToProps)(Login);
I basically am building a web application using react and material UI. I have a registration form.
When this registration form successfully submits, it should display a success message at the top of the page.
Now, I have cordoned off the top of every page to show the MyAlert component. I have done this to make my site more consistent. Basically whenever there is a message to show to the user, it will render there.
Now, the issue is once the message is shown, if I navigate around my site, there is no way to turn it off.
Could someone please help explain what the best way to show a message once, then hide it would be? Basically once the user registers successfully, show a success message. Once the user navigates to a new page, hide the message at the top until a new message needs to be shown.
I imagine this is a very common design pattern, so I must be missing something in terms of React knowledge. Even best practice design patterns, or links to how it can be handled would be appreciated.
My toy app looks as follows:
App.jsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { Alert } from '#material-ui/lab'
import MenuIcon from '#material-ui/icons/Menu'
import {
Button,
AppBar,
Toolbar,
IconButton,
Typography,
makeStyles,
Container,
} from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}))
const Navbar = (props) => {
const classes = useStyles()
return (
<div>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
<Button color="inherit" component={Link} to={'/'}>
My App
</Button>
</Typography>
<Button color="inherit" component={Link} to={'/register'}>
Register
</Button>
</Toolbar>
</AppBar>
</div>
)
}
const Register = (props) => {
const classes = useStyles()
const { setShowAlert, setAlertMessage, setAlertSeverity } = props
useEffect(() => {
console.log('use effected')
}, [])
const handleRegistration = () => {
setAlertMessage(
'You have succesfully registered! Please check your email for a verification link.'
)
setAlertSeverity('success')
setShowAlert(true)
}
return (
<Container maxWidth="sm" align="center" className={classes.root}>
This is some kind of form. I have redacted all input fields to
illustrate the issue more clearly.<br></br>
<Button
variant="contained"
color="primary"
onClick={handleRegistration}
>
Register
</Button>
</Container>
)
}
const MyAlert = (props) => {
const { severity, message } = props
return <Alert severity={severity}>{message}</Alert>
}
const App = () => {
const [alertSeverity, setAlertSeverity] = useState('')
const [alertMessage, setAlertMessage] = useState('')
const [showAlert, setShowAlert] = useState(false)
return (
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert ? (
<MyAlert severity={alertSeverity} message={alertMessage} />
) : (
''
)}
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
)
}
export default App
index.jsx
import * as React from "react"
import { render } from "react-dom"
import App from './App';
render(<App />, document.getElementById("app"))
Wouldn't that just be the case of using a setTimeout? When MyAlert is rendered, show it for 5 seconds and then dismiss.
First, pass it as a prop to your component (I also proposed a slightly different conditional render):
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert &&
<MyAlert severity={alertSeverity} message={alertMessage} setShowAlert = {setShowAlert}/> }
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
Then in your MyAlert component, use useEffect to trigger a timeout:
const MyAlert = (props) => {
useEffect(() => {
const timeout = setTimeout(() => {
props.setShowAlert(false); // Disable your alert after 5 seconds
}, 5000);
return () => {
clearTimeout(timeout); // Clears timer in case you close your alert somewhere else.
}
}, [])
}
I have a problem on redirect to the previous page since i do a check isLoggedIn. The problem right now is after check isLoggedIn it redirect to the default route. How do i maintain the page where I'm into?
What i did right now is using the referer but it is undefined. Pls help me find another way.
Pls check my code below:
Login.js
const Form = (props) => {
const classes = useStyles();
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
const referer = props.referer;
const history = useHistory();
console.log(history);
const { values, touched, errors, isSubmitting, handleChange, handleBlur, handleSubmit } = props;
if (isLoggedIn) {
return <Redirect to={referer} />;
}
return (
<div className={classes.root}>
<Grid container direction="row" justify="center">
<Grid item lg={4} md={5} xs={10}>
<Card>
<form onSubmit={handleSubmit}>
<CardHeader
title="LOGIN"
classes={{
title: classes.cardHeader,
}}
className={classes.cardHeader}
/>
<CardContent className={classes.textFieldSection}>
<TextField
fullWidth
label="Username"
name="username"
type="text"
variant="outlined"
value={values.username}
onChange={handleChange}
onBlur={handleBlur}
helperText={touched.username ? errors.username : ''}
error={touched.username && Boolean(errors.username)}
InputProps={{
endAdornment: (
<InputAdornment>
<AccountCircle />
</InputAdornment>
),
}}
/>
<TextField
fullWidth
label="Password"
name="password"
style={{ marginTop: '1rem' }}
type="password"
variant="outlined"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
helperText={touched.password ? errors.password : ''}
error={touched.password && Boolean(errors.password)}
InputProps={{
endAdornment: (
<InputAdornment>
<LockIcon />
</InputAdornment>
),
}}
/>
</CardContent>
<CardActions className={classes.loginButtonSection}>
<Button
type="submit"
color="primary"
variant="contained"
className={classes.loginButton}
disabled={isSubmitting}
>
Log In
</Button>
</CardActions>
</form>
</Card>
</Grid>
</Grid>
</div>
);
};
let yup = require('yup');
export const Login = (props) => {
const dispatch = useDispatch();
const MyFormWithFormik = withFormik({
mapPropsToValues: ({ username, password }) => {
return {
username: username || '',
password: password || '',
};
},
validationSchema: yup.object().shape({
username: yup.string().required('Enter your username'),
password: yup.string().required('Enter your password'),
}),
handleSubmit: (values, { setSubmitting }) => {
dispatch(login(values.username, values.password));
setSubmitting(false);
},
})(Form);
return <MyFormWithFormik />;
};
Form.propTypes = {
className: PropTypes.string,
};
export default Login;
PrivateRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import { useSelector } from 'react-redux';
function PrivateRoute({ component: Component, ...rest }) {
const authTokens = useSelector((state) => state.auth.access_token);
function checkTokenExpiry() {
if (authTokens) {
axios.interceptors.request.use(
function (config) {
const token = `Bearer ${authTokens}`;
config.headers.Authorization = token;
console.log(config);
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
}
);
}
}
if (authTokens != null) {
checkTokenExpiry();
}
return (
<Route
{...rest}
render={(props) =>
authTokens ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { referer: props.location } }} />
)
}
/>
);
}
export default PrivateRoute;
PrivateAdminOnlyRoute
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import axios from 'axios';
import { useSelector } from 'react-redux';
function PrivateAdminOnlyRoute({ component: Component, ...rest }) {
const authTokens = useSelector((state) => state.auth.access_token);
const isAdmin = useSelector((state) => state.auth.is_admin);
function checkTokenExpiry() {
if (authTokens) {
axios.interceptors.request.use(
function (config) {
const token = `Bearer ${authTokens}`;
config.headers.Authorization = token;
console.log(config);
return config;
},
(error) => {
console.log(error);
Promise.reject(error);
}
);
}
}
if (authTokens != null) {
checkTokenExpiry();
}
return (
<Route
{...rest}
render={(props) =>
authTokens && isAdmin === true ? (
<Component {...props} />
) : authTokens && (isAdmin === false || undefined || null) ? (
<Redirect to={{ pathname: '/pending', state: { referer: props.location } }} />
) : (
<Redirect to={{ pathname: '/login', state: { referer: props.location } }} />
)
}
/>
);
}
export default PrivateAdminOnlyRoute;
Routes.js
import React, { useState } from 'react';
import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import Login from './pages/Login/Login';
import Signup from './pages/Signup/Signup';
import Common from './pages/Common';
function Routes() {
return (
<Router>
<Switch>
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<PrivateRoute path="/" component={Common} />
</Switch>
</Router>
);
}
export default Routes;
Using state inside Redirect isn't assigan state to props but to location
import { useLocation } from 'react-router-dom';
const location = useLocation();
const referer = location.state && location.state.referer
another thing about your code (that not connect directly to your issue) is
(isAdmin === false || undefined || null)
isAdmin compared just to false, and in case isAdmin isn't false it is not being compared to undefined, but undefined stand by itself (with falsy behavior...), and so goes for null
hope it's helpful :)
I am working on the react project. Currently, I am struggling to render the nav again after sign in to show the links that are only visible for logged in users. The following code works fine, and also the redirect goes to "/".
The main issue is that the links for logged-in users only visible after a reload of the page.
Nav.js
import React, {Component} from "react";
import {Nav, Navbar} from 'react-bootstrap';
import { Auth } from 'aws-amplify'
import SignIn from "./Authentication/SignIn";
import SignUp from "./Authentication/SignUp";
import SignOut from "./Authentication/SignOut";
import {
BrowserRouter as Router,
withRouter,
Redirect,
Switch,
Route,
Link
} from "react-router-dom";
import Share from "./Share";
import Subscribe from "./Subscribe";
import Home from "./Home"
class PrivateRoute extends React.Component {
constructor (props) {
super(props);
this.state = {
loaded: false,
isAuthenticated: false
}
}
componentDidMount() {
this.authenticate()
this.unlisten = this.props.history.listen(() => {
Auth.currentAuthenticatedUser()
.then(user => console.log('user: ', user))
.catch(() => {
if (this.state.isAuthenticated) this.setState({ isAuthenticated: false })
})
});
}
componentWillUnmount() {
this.unlisten()
}
authenticate() {
Auth.currentAuthenticatedUser()
.then(() => {
this.setState({ loaded: true, isAuthenticated: true});
})
.catch(() => this.props.history.push('/signup'))
}
render() {
const { component: Component, ...rest } = this.props
const { loaded , isAuthenticated} = this.state
if (!loaded) return null
return (
<Route
{...rest}
render={props => {
return isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/signup",
}}
/>
)
}}
/>
)
}
}
PrivateRoute = withRouter(PrivateRoute)
class Routes extends React.Component {
constructor (props) {
super(props);
this.state = {
showItemInMenu: false
}
}
componentDidMount() {
Auth.currentAuthenticatedUser()
.then(() => { this.setState({showItemInMenu: true })})
.catch(() => { this.setState({showItemInMenu: false})});
}
render() {
const showItemInMenu = this.state.showItemInMenu
return (
<Router>
<Navbar bg="dark" variant="dark">
<Navbar.Brand as={Link} to="/">Todo</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav"/>
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto">
<Nav.Link as={Link} to="/">Home</Nav.Link>
{showItemInMenu && <Nav.Link as={Link} to="/share" >Share</Nav.Link>}
{showItemInMenu && <Nav.Link as={Link} to="/subscribe" >Subscribe</Nav.Link> }
{showItemInMenu && <Nav.Link as={Link} to="/signout" >Logout</Nav.Link> }
<Nav.Link as={Link} to="/signup" >Registration</Nav.Link>
<Nav.Link as={Link} to="/signin" >Login</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<Switch>
<Route exact path='/' component={Home}/>
<Route path='/signup' component={SignUp}/>
<Route path='/signin' component={SignIn}/>
<PrivateRoute path='/share' component={Share}/>
<PrivateRoute path='/subscribe' component={Subscribe}/>
<PrivateRoute path='/signout' component={SignOut}/>
</Switch>
</Router>
)
}
}
export default Routes
Signin.js
import React, { Component } from "react";
import { Form, Row, Col,Button, Alert,Container } from 'react-bootstrap';
import { Auth } from "aws-amplify";
import styled from 'styled-components';
import { Redirect } from 'react-router-dom'
class SignIn extends Component {
constructor (props) {
super(props);
this.state = {
username: '',
password: '',
message: '',
redirect: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSignIn = this.handleSignIn.bind(this);
}
handleChange (event) {
this.setState({ [event.target.name]: event.target.value });
}
handleSignIn (event){
event.preventDefault();
const username = this.state.username;
const password = this.state.password;
// You can pass an object which has the username, password and validationData which is sent to a PreAuthentication Lambda trigger
Auth.signIn({
username,
password
})
.then(user => console.log(user))
.then(() => { this.setState({ redirect: true })
})
.catch(err =>
this.setState({message: err.message})
);
};
renderRedirect = () => {
if (this.state.redirect) {
return <Redirect
to={{
pathname: "/",
state: { fromlogin: true }
}}
/>
}
}
render () {
const Title = styled.div`
margin-top: 10px;
margin-bottom: 15px;
`;
return (
<Container>
<Row>
<Col md={6}>
<Title>
<h3>Login Form</h3>
</Title>
<Form onSubmit={this.handleSignIn}>
<Form.Group>
<Form.Control type="text" name="username" placeholder="Username" onChange={this.handleChange} />
</Form.Group>
<Form.Group>
<Form.Control type="password" name="password" placeholder="Password" onChange={this.handleChange} />
</Form.Group>
<Form.Group>
<Button
variant="primary"
type="submit"
value="SIGN IN"
>LOGIN</Button>
</Form.Group>
</Form>
{this.state.message ?
<Alert variant="danger">{this.state.message}</Alert>
: <></>}
</Col>
</Row>
{this.renderRedirect()}
</Container>
);
}
}
export default SignIn
I was able to solve this using React JS context API and local storage for storing the required state globally so that it does not revert on refresh.
Scenario: A top navbar containing site title is to visible every time regardless of user being signed in or not. The username and a logout button to appear in the top navbar only for logged in users. A side menu bar visible only for signed in users.
Issue: The username only appears after a reload to the main page just like the links in the above question.
Solution:
Create a file to store user context
UserContext.js
let reducer = (user, newUser) => {
if (newUser === null) {
localStorage.removeItem("user");
return newUser;
}
return { ...user, ...newUser };
};
const userState = {
name: "",
email: "",
};
const localState = JSON.parse(localStorage.getItem("user"));
const AuthContext = createContext();
const AuthProvider = (props) =>{
const [user, setUser] = useReducer(reducer, localState || userState);
useEffect(() => {
localStorage.setItem("user", JSON.stringify(user));
}, [user]);
return(
<AuthContext.Provider value={{ user, setUser }}>
{props.children}
</AuthContext.Provider>
)
}
export { AuthContext, AuthProvider };
Now in your App.js file wrap this AuthProvider over the components where user context is intended to be accessed.
App.js
function App() {
return (
<AuthProvider>
<Router>
<NavBar />
<Switch>
<Route path="/signin" exact component={SignIn} />
<Route path="/signup" component={SignUp} />
<ProtectedRoute path="/home" component={Home} />
</Switch>
</Router>
</AuthProvider>
);
}
Now consume the context in the components that were wrapped with the context like the Home and Navbar component.
NavBar.js
export default function MiniDrawer() {
const {user, setUser} = useContext(AuthContext)
//set user context and local/global storage respective value to null on signout
const handleSignOut = async () =>
{
try
{
await Auth.signOut()
localStorage.removeItem("user");
setUser(null)
history.push("/signin")
}catch(err)
{
console.log(err.message)
alert('Could not sign out due to: ', err.message)
}
}
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
{ user && user.name &&
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
}
<Typography variant="h6" noWrap>
The Video Site
</Typography>
{ user && user.name &&
<div className={classes.userArea}>
<Typography >
Welcome, {user && user.name}
</Typography>
<ListItem button onClick={handleSignOut}>
<ListItemIcon>{<ExitToAppIcon /> }</ListItemIcon>
<ListItemText primary="Log out" />
</ListItem>
</div>
}
</Toolbar>
</AppBar>
{ user && user.name &&
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
</IconButton>
</div>
<Divider />
<List>
{
SideBarData.map( (item) => {
return(
<Link to={item.path}>
<Tooltip title={item.title} aria-label="add">
<ListItem button key={item.title} >
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
</Tooltip>
</Link>
)
}
)
}
</List>
<Divider />
</Drawer>
}
<main className={classes.content}>
<div className={classes.toolbar} />
{/* <MediaUpload /> */}
</main>
</div>
);
}
Now, you have implemented context with local storage in order to maintain state even after refreshing.
Guidance source