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.
1npm i @material-ui/core @material-ui/lab
Please note that this guide assumes you have a fair understanding of modern ES6 syntax.
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.
1import React, { useState } from "react";
2import { TextField, Typography, Button } from "@material-ui/core";
3import { connect } from "react-redux";
4import { login } from "../actions/auth";
5import MuiAlert from "@material-ui/lab/Alert";
6
7function Alert(props) {
8 return <MuiAlert elevation={6} variant="filled" {...props} />;
9}
10
11export default connect(null, { login })(props => {
12 const [email, setEmail] = useState("");
13 const [password, setPassword] = useState("");
14 const [error, setError] = useState("");
15
16 const submitForm = () => {
17 if (email === "" || password === "") {
18 setError("Fields are required");
19 return;
20 }
21 props.login({ email, password });
22 };
23
24 return (
25 <form>
26 <Typography variant="h5" style={{ marginBottom: 8 }}>
27 Login
28 </Typography>
29 <TextField
30 label="Email"
31 variant="outlined"
32 fullWidth
33 className="form-input"
34 value={email}
35 onChange={e => setEmail(e.target.value)}
36 />
37 <TextField
38 label="Password"
39 variant="outlined"
40 fullWidth
41 className="form-input"
42 type="password"
43 value={password}
44 onChange={e => setPassword(e.target.value)}
45 />
46 <Button
47 variant="contained"
48 color="primary"
49 fullWidth
50 className="form-input"
51 size="large"
52 onClick={submitForm}
53 >
54 Login
55 </Button>
56
57 {error && (
58 <Alert severity="error" onClick={() => setError(null)}>
59 {props.error || error}
60 </Alert>
61 )}
62 </form>
63 );
64});
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.
1export const LOGIN = "LOGIN";
2export const LOGOUT = "LOGOUT";
3
4export const login = user => {
5 return {
6 type: LOGIN,
7 payload: user
8 };
9};
10
11export const logout = () => {
12 return {
13 type: LOGOUT
14 };
15};
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.
1import { apiRequest } from "../actions/api";
2import { LOGIN } from "../actions/auth";
3
4const SERVER_URL = `https://61m46.sse.codesandbox.io`;
5
6export const appMiddleware = () => next => action => {
7 next(action);
8 switch (action.type) {
9 case LOGIN: {
10 next(
11 apiRequest({
12 url: `${SERVER_URL}/login`,
13 method: "POST",
14 data: action.payload
15 })
16 );
17 break;
18 }
19 default:
20 break;
21 }
22};
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.
1import { SET_LOADER } from "./actions/ui";
2import { API_SUCCESS, API_ERROR } from "./actions/api";
3import { LOGOUT } from "./actions/auth";
4
5export default (
6 state = {
7 isAuthUser: !!localStorage.getItem("user"),
8 user: JSON.parse(localStorage.getItem("user")) || {},
9 isLoading: false,
10 error: null
11 },
12 action
13) => {
14 switch (action.type) {
15 case API_SUCCESS:
16 localStorage.setItem("user", JSON.stringify(action.payload.user));
17 return { ...state, isAuthUser: true, user: action.payload.user };
18 case API_ERROR:
19 return { ...state, error: action.payload };
20 case SET_LOADER:
21 return { ...state, isLoading: action.payload };
22 case LOGOUT:
23 localStorage.removeItem("user");
24 return { ...state, isAuthUser: false, user: {} };
25 default:
26 return state;
27 }
28};
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.
1import React from "react";
2import { connect } from "react-redux";
3import { Redirect, Route } from "react-router";
4
5const AuthRoute = props => {
6 const { isAuthUser, type } = props;
7 if (type === "guest" && isAuthUser) return <Redirect to="/home" />;
8 else if (type === "private" && !isAuthUser) return <Redirect to="/" />;
9
10 return <Route {...props} />;
11};
12
13const mapStateToProps = ({ isAuthUser }) => ({
14 isAuthUser
15});
16
17export default connect(mapStateToProps)(AuthRoute);
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.
1export default function App() {
2 return (
3 <Provider store={store}>
4 <Router>
5 <NavBar />
6 <div className="container">
7 <Switch>
8 <AuthRoute path="/login" type="guest">
9 <LoginPage />
10 </AuthRoute>
11 <AuthRoute path="/home" render={HomePage} type="private" />
12 <AuthRoute path="/my-account" type="private">
13 <MyAccount />
14 </AuthRoute>
15 <Route path="/" render={IndexPage} />
16 </Switch>
17 </div>
18 </Router>
19 </Provider>
20 );
21}
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
.
1import React from "react";
2import ReactDOM from "react-dom";
3
4import App from "./App";
5
6const rootElement = document.getElementById("root");
7ReactDOM.render(<App />, rootElement);
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.
1import React from "react";
2import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
3import "./app.css";
4import { Provider } from "react-redux";
5import { applyMiddleware } from "redux";
6
7import reducer from "./reducer";
8import { createStore } from "redux";
9
10import NavBar from "./components/Nav";
11import { Typography, Divider } from "@material-ui/core";
12
13import AuthRoute from "./components/AuthRoute";
14
15import HomePage from "./pages/HomePage";
16import LoginPage from "./pages/Login";
17
18import { appMiddleware } from "./middlewares/app";
19import { apiMiddleware } from "./middlewares/core";
20import MyAccount from "./pages/MyAccount";
21
22const createStoreWithMiddleware = applyMiddleware(
23 appMiddleware,
24 apiMiddleware
25)(createStore);
26
27const store = createStoreWithMiddleware(reducer);
28
29const IndexPage = () => (
30 <>
31 <Typography variant="h3">Welcome to the App</Typography>
32 <Divider style={{ marginTop: 10, marginBottom: 10 }} />
33 <Typography variant="h6">Feel free to take a look around</Typography>
34 </>
35);
36
37export default function App() {
38 return (
39 <Provider store={store}>
40 <Router>
41 <NavBar />
42 <div className="container">
43 <Switch>
44 <AuthRoute path="/home" render={HomePage} type="private" />
45 <AuthRoute path="/login" type="guest">
46 <LoginPage />
47 </AuthRoute>
48 <AuthRoute path="/my-account" type="private">
49 <MyAccount />
50 </AuthRoute>
51 <Route path="/" render={IndexPage} />
52 </Switch>
53 </div>
54 </Router>
55 </Provider>
56 );
57}
Notice that we have used the <Router />
and <Switch />
components from react-router-dom
for leveraging client-side routing.
1import { SET_LOADER } from "./actions/ui";
2import { API_SUCCESS, API_ERROR } from "./actions/api";
3import { LOGOUT } from "./actions/auth";
4
5export default (
6 state = {
7 isAuthUser: !!localStorage.getItem("user"),
8 user: JSON.parse(localStorage.getItem("user")) || {},
9 isLoading: false,
10 error: null
11 },
12 action
13) => {
14 switch (action.type) {
15 case API_SUCCESS:
16 localStorage.setItem("user", JSON.stringify(action.payload.user));
17 return { ...state, isAuthUser: true, user: action.payload.user };
18 case API_ERROR:
19 return { ...state, error: action.payload };
20 case SET_LOADER:
21 return { ...state, isLoading: action.payload };
22 case LOGOUT:
23 localStorage.removeItem("user");
24 return { ...state, isAuthUser: false, user: {} };
25 default:
26 return state;
27 }
28};
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.
1import { apiRequest } from "../actions/api";
2import { LOGIN } from "../actions/auth";
3
4const SERVER_URL = `https://61m46.sse.codesandbox.io`;
5
6export const appMiddleware = () => next => action => {
7 next(action);
8 switch (action.type) {
9 case LOGIN: {
10 next(
11 apiRequest({
12 url: `${SERVER_URL}/login`,
13 method: "POST",
14 data: action.payload
15 })
16 );
17 break;
18 }
19 default:
20 break;
21 }
22};
1import axios from "axios";
2import { API_REQUEST, apiError, apiSuccess } from "../actions/api";
3import { setLoader } from "../actions/ui";
4
5export const apiMiddleware = ({ dispatch }) => next => action => {
6 next(action);
7
8 if (action.type === API_REQUEST) {
9 dispatch(setLoader(true));
10 const { url, method, data } = action.meta;
11 axios({
12 method,
13 url,
14 data
15 })
16 .then(({ data }) => dispatch(apiSuccess({ response: data })))
17 .catch(error => {
18 console.log(error);
19 dispatch(apiError({ error: error.response.data }));
20 });
21 }
22};
COMPONENTS
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.
1import React from "react";
2import { connect } from "react-redux";
3import { Redirect, Route } from "react-router";
4
5const AuthRoute = props => {
6 const { isAuthUser, type } = props;
7 if (type === "guest" && isAuthUser) return <Redirect to="/home" />;
8 else if (type === "private" && !isAuthUser) return <Redirect to="/" />;
9
10 return <Route {...props} />;
11};
12
13const mapStateToProps = ({ isAuthUser }) => ({
14 isAuthUser
15});
16
17export 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.
1import React, { Component } from "react";
2import { Link } from "react-router-dom";
3import { AppBar, Toolbar, Button, Typography } from "@material-ui/core";
4import { connect } from "react-redux";
5
6import { logout } from "../actions/auth";
7
8class NavBar extends Component {
9 render() {
10 return (
11 <AppBar position="static" style={{ display: "flex" }}>
12 <Toolbar>
13 <Typography variant="h6">My App</Typography>
14 <div style={{ marginLeft: "auto" }}>
15 {this.props.isAuthUser ? (
16 <>
17 <Link to="/home">
18 <Button color="inherit">Home</Button>
19 </Link>
20 <Link to="/my-account">
21 <Button color="inherit">My Account</Button>
22 </Link>
23 <Button color="inherit" onClick={this.props.logout}>
24 Logout
25 </Button>
26 </>
27 ) : (
28 <Link to="/login">
29 <Button color="inherit">Login</Button>
30 </Link>
31 )}
32 </div>
33 </Toolbar>
34 </AppBar>
35 );
36 }
37}
38
39export default connect(({ isAuthUser }) => ({ isAuthUser }), { logout })(
40 NavBar
41);
PAGES
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.
1import React, { useState } from "react";
2import { TextField, Typography, Button } from "@material-ui/core";
3import { connect } from "react-redux";
4import { login } from "../actions/auth";
5import MuiAlert from "@material-ui/lab/Alert";
6
7function Alert(props) {
8 return <MuiAlert elevation={6} variant="filled" {...props} />;
9}
10
11export default connect(({ isLoading }) => ({ isLoading }), { login })(props => {
12 const [email, setEmail] = useState("");
13 const [password, setPassword] = useState("");
14 const [error, setError] = useState("");
15
16 const submitForm = () => {
17 if (email === "" || password === "") {
18 setError("Fields are required");
19 return;
20 }
21 props.login({ email, password });
22 };
23
24 return (
25 <form>
26 <Typography variant="h5" style={{ marginBottom: 8 }}>
27 Login
28 </Typography>
29 <TextField
30 label="Email"
31 variant="outlined"
32 fullWidth
33 className="form-input"
34 value={email}
35 onChange={e => setEmail(e.target.value)}
36 />
37 <TextField
38 label="Password"
39 variant="outlined"
40 fullWidth
41 className="form-input"
42 type="password"
43 value={password}
44 onChange={e => setPassword(e.target.value)}
45 />
46 <Button
47 variant="contained"
48 color="primary"
49 fullWidth
50 className="form-input"
51 size="large"
52 onClick={submitForm}
53 >
54 Login
55 </Button>
56
57 {(props.error || error) && (
58 <Alert severity="error" onClick={() => setError(null)}>
59 {props.error || error}
60 </Alert>
61 )}
62 </form>
63 );
64});
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.