How to Router Redirect After Login
Nov 12, 2020 • 18 Minute Read
Introduction
With the popularity of Redux and React increasing with each passing day, it's a no-brainer to give them the attention they deserve. React uses Redux's state for maintaining state throughout the app. The purpose of the state is to keep your application state synchronized with the Redux store.
In this guide, we are going to learn how to redirect a user after a successful login.. Usually, when we are building web apps, there's a requirement that the user must be logged in to use the app. In that case, we need to take care of the user's identity and manage his authentication token in the application state and redirect the user to protected routes.
For styling this demo, I'll be using material-ui. Please run the following command to add it to your dependencies.
npm i @material-ui/core @material-ui/lab
Please note that this guide assumes you have a fair understanding of modern ES6 syntax.
Login Form
The login form has two fields: Email and Password. When the user clicks on the Submit button, we will dispatch a login action with the type LOGIN and payload as the form values. To manage the state in the component, I have used React hooks, which is now a default method for managing state in functional components.
import React, { useState } from "react";
import { TextField, Typography, Button } from "@material-ui/core";
import { connect } from "react-redux";
import { login } from "../actions/auth";
import MuiAlert from "@material-ui/lab/Alert";
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
export default connect(null, { login })(props => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const submitForm = () => {
if (email === "" || password === "") {
setError("Fields are required");
return;
}
props.login({ email, password });
};
return (
<form>
<Typography variant="h5" style={{ marginBottom: 8 }}>
Login
</Typography>
<TextField
label="Email"
variant="outlined"
fullWidth
className="form-input"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<TextField
label="Password"
variant="outlined"
fullWidth
className="form-input"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<Button
variant="contained"
color="primary"
fullWidth
className="form-input"
size="large"
onClick={submitForm}
>
Login
</Button>
{error && (
<Alert severity="error" onClick={() => setError(null)}>
{props.error || error}
</Alert>
)}
</form>
);
});
Actions and Middlewares
As in this previous guide, we'll use middleware to handle the network requests to send the login details to our server. Check out the other guide if you are not familiar with middlewares in Redux.
auth.js
export const LOGIN = "LOGIN";
export const LOGOUT = "LOGOUT";
export const login = user => {
return {
type: LOGIN,
payload: user
};
};
export const logout = () => {
return {
type: LOGOUT
};
};
When the LOGIN action is dispatched, we will catch the action in out middleware and dispatch the API_REQUEST action along with the login form values data. If the request is successful, we will dispatch the API_SUCCESS action, and if there is an error, we will dispatch the API_ERROR action.
app.js
import { apiRequest } from "../actions/api";
import { LOGIN } from "../actions/auth";
const SERVER_URL = `https://61m46.sse.codesandbox.io`;
export const appMiddleware = () => next => action => {
next(action);
switch (action.type) {
case LOGIN: {
next(
apiRequest({
url: `${SERVER_URL}/login`,
method: "POST",
data: action.payload
})
);
break;
}
default:
break;
}
};
Reducer Function
In the reducer function, we will set the user's AUTH token, which is sent by the server. In a real-world application, the token is saved in the cookie for security reasons, but for simplicity, we are going to store it in local storage. While setting the default state, we check the local storage for whether the user token exists or not.
import { SET_LOADER } from "./actions/ui";
import { API_SUCCESS, API_ERROR } from "./actions/api";
import { LOGOUT } from "./actions/auth";
export default (
state = {
isAuthUser: !!localStorage.getItem("user"),
user: JSON.parse(localStorage.getItem("user")) || {},
isLoading: false,
error: null
},
action
) => {
switch (action.type) {
case API_SUCCESS:
localStorage.setItem("user", JSON.stringify(action.payload.user));
return { ...state, isAuthUser: true, user: action.payload.user };
case API_ERROR:
return { ...state, error: action.payload };
case SET_LOADER:
return { ...state, isLoading: action.payload };
case LOGOUT:
localStorage.removeItem("user");
return { ...state, isAuthUser: false, user: {} };
default:
return state;
}
};
HOC for Authentication
To authenticate the user across pages, we need to create a higher-order component (HOC) to wrap the <Router /> component. You might be thinking, why do we have to authenticate the user for every page? Well, that's because, in a single-page app, we need to maintain the user's session on the client-side.
There are two types of routes when it comes to authentication. One is a guest route, which can only be accessed by guest users, such as the login page or register page. The second is a private route, which can only be accessed by an authenticated user.
If the user is not authenticated, we will redirect to the index page; otherwise, we will redirect to the home page.
import React from "react";
import { connect } from "react-redux";
import { Redirect, Route } from "react-router";
const AuthRoute = props => {
const { isAuthUser, type } = props;
if (type === "guest" && isAuthUser) return <Redirect to="/home" />;
else if (type === "private" && !isAuthUser) return <Redirect to="/" />;
return <Route {...props} />;
};
const mapStateToProps = ({ isAuthUser }) => ({
isAuthUser
});
export default connect(mapStateToProps)(AuthRoute);
Using the Component
In the <App /> component, we will define all the routes in our application. We will use the <AuthRoute /> component that we created in the previous section to specify the type of route, whether it is a private or a guest route.
export default function App() {
return (
<Provider store={store}>
<Router>
<NavBar />
<div className="container">
<Switch>
<AuthRoute path="/login" type="guest">
<LoginPage />
</AuthRoute>
<AuthRoute path="/home" render={HomePage} type="private" />
<AuthRoute path="/my-account" type="private">
<MyAccount />
</AuthRoute>
<Route path="/" render={IndexPage} />
</Switch>
</div>
</Router>
</Provider>
);
}
Complete Source Code
index.js
index.js will be the entry file of our web application. Here, we will mount the root component to an element, i.e., a <div> with an id of root.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
App.js
In the App.js file, we have defined the main or the root component, i.e., the <App /> component. We will be wrapping all the child components in the <Provider /> component from the react-redux library to make the global redux store available throughout the application.
import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import "./app.css";
import { Provider } from "react-redux";
import { applyMiddleware } from "redux";
import reducer from "./reducer";
import { createStore } from "redux";
import NavBar from "./components/Nav";
import { Typography, Divider } from "@material-ui/core";
import AuthRoute from "./components/AuthRoute";
import HomePage from "./pages/HomePage";
import LoginPage from "./pages/Login";
import { appMiddleware } from "./middlewares/app";
import { apiMiddleware } from "./middlewares/core";
import MyAccount from "./pages/MyAccount";
const createStoreWithMiddleware = applyMiddleware(
appMiddleware,
apiMiddleware
)(createStore);
const store = createStoreWithMiddleware(reducer);
const IndexPage = () => (
<>
<Typography variant="h3">Welcome to the App</Typography>
<Divider style={{ marginTop: 10, marginBottom: 10 }} />
<Typography variant="h6">Feel free to take a look around</Typography>
</>
);
export default function App() {
return (
<Provider store={store}>
<Router>
<NavBar />
<div className="container">
<Switch>
<AuthRoute path="/home" render={HomePage} type="private" />
<AuthRoute path="/login" type="guest">
<LoginPage />
</AuthRoute>
<AuthRoute path="/my-account" type="private">
<MyAccount />
</AuthRoute>
<Route path="/" render={IndexPage} />
</Switch>
</div>
</Router>
</Provider>
);
}
Notice that we have used the <Router /> and <Switch /> components from react-router-dom for leveraging client-side routing.
reducer.js
import { SET_LOADER } from "./actions/ui";
import { API_SUCCESS, API_ERROR } from "./actions/api";
import { LOGOUT } from "./actions/auth";
export default (
state = {
isAuthUser: !!localStorage.getItem("user"),
user: JSON.parse(localStorage.getItem("user")) || {},
isLoading: false,
error: null
},
action
) => {
switch (action.type) {
case API_SUCCESS:
localStorage.setItem("user", JSON.stringify(action.payload.user));
return { ...state, isAuthUser: true, user: action.payload.user };
case API_ERROR:
return { ...state, error: action.payload };
case SET_LOADER:
return { ...state, isLoading: action.payload };
case LOGOUT:
localStorage.removeItem("user");
return { ...state, isAuthUser: false, user: {} };
default:
return state;
}
};
MIDDLEWARES
We'll be creating two types of redux middlewares: an appMiddleware and a coreMiddleware. The appMiddeware will be responsible for handling the API requests. In this case, we pass the relevant data for the API request through the LOGIN action, and in the coreMiddleware, we catch the API_REQUEST action and make the network request using the axios HTTP library.
app.js
import { apiRequest } from "../actions/api";
import { LOGIN } from "../actions/auth";
const SERVER_URL = `https://61m46.sse.codesandbox.io`;
export const appMiddleware = () => next => action => {
next(action);
switch (action.type) {
case LOGIN: {
next(
apiRequest({
url: `${SERVER_URL}/login`,
method: "POST",
data: action.payload
})
);
break;
}
default:
break;
}
};
core.js
import axios from "axios";
import { API_REQUEST, apiError, apiSuccess } from "../actions/api";
import { setLoader } from "../actions/ui";
export const apiMiddleware = ({ dispatch }) => next => action => {
next(action);
if (action.type === API_REQUEST) {
dispatch(setLoader(true));
const { url, method, data } = action.meta;
axios({
method,
url,
data
})
.then(({ data }) => dispatch(apiSuccess({ response: data })))
.catch(error => {
console.log(error);
dispatch(apiError({ error: error.response.data }));
});
}
};
COMPONENTS
AuthRoute.js
The <AuthRoute /> component is a higher-order component that wraps the <Route /> component of react-router to keep the routes specific to our application as private or public.
import React from "react";
import { connect } from "react-redux";
import { Redirect, Route } from "react-router";
const AuthRoute = props => {
const { isAuthUser, type } = props;
if (type === "guest" && isAuthUser) return <Redirect to="/home" />;
else if (type === "private" && !isAuthUser) return <Redirect to="/" />;
return <Route {...props} />;
};
const mapStateToProps = ({ isAuthUser }) => ({
isAuthUser
});
export default connect(mapStateToProps)(AuthRoute);
In the <Navbar /> component, we are merely creating the navigation menu for our application. Notice that I have used the <AppBar /> component from material-ui to give it a native look.
import React, { Component } from "react";
import { Link } from "react-router-dom";
import { AppBar, Toolbar, Button, Typography } from "@material-ui/core";
import { connect } from "react-redux";
import { logout } from "../actions/auth";
class NavBar extends Component {
render() {
return (
<AppBar position="static" style={{ display: "flex" }}>
<Toolbar>
<Typography variant="h6">My App</Typography>
<div style={{ marginLeft: "auto" }}>
{this.props.isAuthUser ? (
<>
<Link to="/home">
<Button color="inherit">Home</Button>
</Link>
<Link to="/my-account">
<Button color="inherit">My Account</Button>
</Link>
<Button color="inherit" onClick={this.props.logout}>
Logout
</Button>
</>
) : (
<Link to="/login">
<Button color="inherit">Login</Button>
</Link>
)}
</div>
</Toolbar>
</AppBar>
);
}
}
export default connect(({ isAuthUser }) => ({ isAuthUser }), { logout })(
NavBar
);
PAGES
Login.js
In the Login.js file, we are creating the page component that displays the login form. We have used the modern React hooks to leverage state in a functional component. This allows us to make the code more precise and easier to maintain.
import React, { useState } from "react";
import { TextField, Typography, Button } from "@material-ui/core";
import { connect } from "react-redux";
import { login } from "../actions/auth";
import MuiAlert from "@material-ui/lab/Alert";
function Alert(props) {
return <MuiAlert elevation={6} variant="filled" {...props} />;
}
export default connect(({ isLoading }) => ({ isLoading }), { login })(props => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const submitForm = () => {
if (email === "" || password === "") {
setError("Fields are required");
return;
}
props.login({ email, password });
};
return (
<form>
<Typography variant="h5" style={{ marginBottom: 8 }}>
Login
</Typography>
<TextField
label="Email"
variant="outlined"
fullWidth
className="form-input"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<TextField
label="Password"
variant="outlined"
fullWidth
className="form-input"
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<Button
variant="contained"
color="primary"
fullWidth
className="form-input"
size="large"
onClick={submitForm}
>
Login
</Button>
{(props.error || error) && (
<Alert severity="error" onClick={() => setError(null)}>
{props.error || error}
</Alert>
)}
</form>
);
});
Conclusion
In this guide, we took a look at how to create a higher-order component to manage the authentication of a user in a single-page app. Securely storing the AUTH token is an essential factor, and although we have used local storage in this guide, it is recommended that you use cookies. Leveraging Redux middlewares to handle API requests is another skill you need to know, as I have stressed in an earlier article.
That's it from this guide. I believe you can now take your React skills to the next level. If you have any queries, feel free to reach out at Codealphabet.
Learn More
Explore these React and Redux courses from Pluralsight to continue learning: