Web Application using Django. Part 3(b)

Himraj Gogoi
10 min readAug 22, 2021

In the previous half of part 3 of the series, we saw how to configure our project to support React framework to create the front-end. In this final half, we will be creating the front-end using various modules provided by React.

If you haven’t gone through the previous parts, visit:

Lets begin,

Stage 1

  1. In the command prompt, within the project folder, run:
npm install redux react-redux redux-thunk redux-devtools-extension

This will install the necessary middleware which are required to maintain the state in the front-end.

2. In the frontend folder,within the src folder, create a store.js file and a reducers folder.

3. Then, install axios :

npm install axios

4. And finally for error handling, install the necessary modules:

npm install react-alert react-alert-template-basic react-transition-group

Stage 2

  1. Within the src folder, create an actions folder and within it, create a types.js file and add:
export const GET_MESSAGES = "GET_MESSAGES";export const DELETE_MESSAGE = "DELETE_MESSAGE";export const ADD_MESSAGE = "ADD_MESSAGE";export const GET_ERRORS = "GET_ERRORS";export const MESSAGE_FAILED = "MESSAGE_FAILED";export const USER_LOADING = "USER_LOADING";export const USER_LOADED = "USER_LOADED";export const AUTH_ERROR = "AUTH_ERROR";export const LOGIN_SUCCESS = "LOGIN_SUCCESS";export const LOGIN_FAILED = "LOGIN_FAILED";export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS";export const REGISTER_SUCCESS = "REGISTER_SUCCESS";export const REGISTER_FAILED = "REGISTER_FAILED";

These are all the types of action we want to perform.

2. Within the actions folder, create an auth.js file and add:

import axios from 'axios';import {USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT_SUCCESS, GET_ERRORS, REGISTER_SUCCESS, REGISTER_FAILED} from "./types";//Check Token and Load Userexport const loadUser = () => (dispatch, getState) =>{    //User Loading    dispatch({type: USER_LOADING});    const config = tokenConfig(getState);    axios.get('/api/auth/user', config)    .then(res=>{        dispatch({type: USER_LOADED,payload: res.data});    })    .catch(err=>{        dispatch({type: AUTH_ERROR,payload: err})    });}//LOGIN USERexport const login = (username, password) => (dispatch) =>{    //Header    const config = {        headers: {            'Content-Type': 'application/json'         }    }    const body = JSON.stringify({username, password});    axios.post('/api/auth/login',body, config)    .then(res=>{        dispatch({type: LOGIN_SUCCESS,payload: res.data});    })    .catch(err=>{        dispatch({type: LOGIN_FAILED,payload: err});        dispatch({type: GET_ERRORS,payload: err.message})    });}//Register Userexport const register = ({username, email, password}) => (dispatch) =>{//Header    const config = {        headers: {            'Content-Type': 'application/json'         }    }    const body = JSON.stringify({username, email, password});    axios.post('/api/auth/register',body, config)    .then(res=>{
dispatch({type: REGISTER_SUCCESS,payload: res.data});
}) .catch(err=>{ dispatch({type: REGISTER_FAILED,payload: err}); dispatch({type: GET_ERRORS,payload: err.message}) });}//LOGOUT USERexport const logout = () => (dispatch, getState) =>{ const config = tokenConfig(getState) axios.post('/api/auth/logout/',null, config) .then(res=>{ dispatch({type: LOGOUT_SUCCESS});
})
.catch(err=>{ dispatch({type: GET_ERRORS,payload: err}) });}//Setup config with token - helper functionexport const tokenConfig = (getState)=>{ //Get token from state const token = getState().auth.token; //Header const config = { headers: { 'Content-Type': 'application/json' } } //If token, add to headers config if(token){ config.headers["Authorization"] = `Token ${token}`; } return config;}

An action is a function which dispatches or sends an object with the following properties:

I. type → which specifies what kind of action it has performed.

II. payload → the response data that is sent to update the state.

Any action must have a type but a payload is optional. In the above code, we have defined various actions related to authentication such as login, register, logout and loadUser.

3. Similarly, create a messages.js file and add:

import axios from "axios";import {GET_MESSAGES, DELETE_MESSAGE, ADD_MESSAGE, GET_ERRORS, MESSAGE_FAILED} from "./types";
import {tokenConfig} from "./auth";
//GET MESSAGESexport const getMessages = ()=> (dispatch, getState) =>{ axios.get('/api/messages/', tokenConfig(getState)) .then(res=>{ dispatch({type: GET_MESSAGES,payload: res.data}); }) .catch(error=>{ dispatch({type: MESSAGE_FAILED,payload: error.message})} );}//DELETE MESSAGEexport const deleteMessages = (id)=> (dispatch, getState) =>{ axios.delete(`/api/messages/${id}`, tokenConfig(getState)) .then(res=>{ dispatch({type: DELETE_MESSAGE,payload: id}); }) .catch(error=>{ dispatch({type: GET_ERRORS,payload: error.message})} );}//ADD MESSAGEexport const addMessage = (message)=> (dispatch, getState) =>{ axios.post('/api/messages/',message, tokenConfig(getState)) .then(res=>{ dispatch({type: ADD_MESSAGE,payload: res.data}); }) .catch(error=>{ dispatch({type: GET_ERRORS,payload: error.message})} );}

4. In the reducers folder, create an auth.js file and add:

import {USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_FAILED, LOGIN_SUCCESS, LOGOUT_SUCCESS, REGISTER_SUCCESS,REGISTER_FAILED} from "../actions/types";const intialState = {    token: localStorage.getItem('token'),    isAuthenticated: false,    isLoading: false,    user: null}export const Auth = (state=intialState, action) =>{    switch (action.type) {        case USER_LOADING:            return {...state, isLoading: true};        case USER_LOADED:            return {...state, isAuthenticated: true, isLoading: false, user:action.payload};        case LOGIN_SUCCESS:        case REGISTER_SUCCESS:            localStorage.setItem('token', action.payload.token);            return {...state,...action.payload, isAuthenticated: true, isLoading: false };        case LOGOUT_SUCCESS:        case REGISTER_FAILED:            localStorage.removeItem('token');            return {...state, token: null, user: null, isAuthenticated: false, isLoading: false};        case AUTH_ERROR:            localStorage.removeItem('token');            return {...state, token: null, user: null, isAuthenticated: false, isLoading: false};        case LOGIN_FAILED:            localStorage.removeItem('token');            return {...state, token: null, user: null, isAuthenticated: false, isLoading: false};        default:            return state;            break;    }}

A reducer is a function that takes a state and an action that we want to perform on the state. State is the data that we are displaying to the users, it can change overtime due to operations on our database like post, put or delete. So we need to update our state to reflect those changes. In the above case, we are defining an initial state with certain properties; the Auth function is the reducer here. As you can see, it takes a state and an action and then we use switch case to take different actions on our state based on the type provided.

5. Create an error.js file and add:

import {GET_ERRORS} from "../actions/types";const intialState = {    msg: null,}export const Error = (state=intialState, action)=>{    switch (action.type) {        case GET_ERRORS:            return {...state, msg: action.payload};        default:            return state;    }}

6. Then, create a messages.js file and add:

import {GET_MESSAGES, DELETE_MESSAGE, ADD_MESSAGE, MESSAGE_FAILED} from "../actions/types.js";const intialState = {    messages: [],    errMess: null}export const Messages = (state= intialState, action)=>{    switch (action.type) {        case  GET_MESSAGES:            return {...state, messages: action.payload, errMess: null };        case DELETE_MESSAGE:            return {...state, messages: state.messages.filter(message=> message.id != action.payload), errMess: null}        case ADD_MESSAGE:            return {...state, messages: [...state.messages, action.payload], errMess: null}        case MESSAGE_FAILED:            return {...state, messages: [], errMess: action.payload}        default:            return state;    }}

7. Finally, in the store.js file, add:

import {createStore, applyMiddleware, combineReducers} from "redux";import { composeWithDevTools } from "redux-devtools-extension";import thunk from "redux-thunk";import {Messages} from "./reducers/messages";import {Error} from "./reducers/errors";import {Auth} from "./reducers/auth";const intialState = {};const middleware = [thunk]const store = createStore(    combineReducers({        messages: Messages,        error: Error,        auth: Auth    }),    intialState,    applyMiddleware(...middleware));export default store;

The redux module provides us with certain methods that help us in creating an efficient environment to update the state of our front-end. The createStore method creates a “store” of all the reducers and the intial state along with any middleware we want to apply. The combineReducers method combines all the reducers into a single object from which we can destructure the various reducers as and when needed.

Stage 3

8. Within the components folder, create a common folder and within it create a PrivateRoutes.js file and add:

import React, {Component} from 'react';import {Route, Redirect} from "react-router-dom";import {connect} from "react-redux";const PrivateRoutes = ({component: Component, auth, ...rest})=> (    <Route    {...rest}    render={props=>{            if(auth.isLoading){                return <h2>Loading...</h2>;            }            else if(!auth.isAuthenticated){                return <Redirect to ="/login"/>;           }            else{                return <Component {...props}/>;            }        }    }/>);const mapStateToProps = state =>({    auth: state.auth})export default connect(mapStateToProps)(PrivateRoutes);

So the common folder contains all the files that are common to the entire front-end app. Here the PrivateRoutes file contains a custom Route. The PrivateRoutes takes in certain parameters. Accordingly, it either provides a Loading text while we authenticate the user, redirects the user to the login page if authentication failed or finally returns the component we want to visit. Also, take note of the mapStateToProps function, it maps our reducer auth, which is provided by the state object to the auth property .i.e “mapping state to props”. And, to make use of this function, we must “ connect” it to our custom Route function.

9. Then within the components folder, create an accounts folder and within it create Login.js and Register.js files. This folder will create the components for authenticating the users.

10. In the Login.js file, add:

import React, { Component } from 'react'import {Link, Redirect} from "react-router-dom";import {connect} from 'react-redux';import {login} from "../../actions/auth";const mapDispatchToProps = dispatch =>({login: (username, password) => dispatch(login(username, password))})export class Login extends Component {    state={        username: '',        password: '',    }    onSubmit = e =>{        e.preventDefault();        this.props.login(this.state.username, this.state.password);        }    onChange = e=> this.setState({[e.target.name]: e.target.value});    render() {        if(this.props.isAuthenticated){            return <Redirect to ="/"/>        }        else{            const username = this.state.username;            const password = this.state.password;            if(this.props.error){                return(                    <div className="col-md-6 m-auto">                    <h4>Enter valid credentials.Refresh.</h4>                    </div>                );             }             return (                <div className="col-md-6 m-auto">                    <div className="card card-body mt-4 mb-4">                        <h1>Login</h1>                         <form onSubmit={this.onSubmit}>                         <div className="form-group">                         <label>Name</label>                         <input                         className="form-control"                         type="text"                         name="username"                         onChange = {this.onChange}                         value={username}/>                    </div>                    <div className="form-group">                        <label>Password</label>                        <input                        className="form-control"                        type="password"                        name="password"                        onChange={this.onChange}                        value={password}/>                    </div>                    <div className="form-group">                        <button type="submit" className="btn btn-primary">                        Login                        </button>                    </div>                    <p>Don't have an Account? <Link to="/register">Register</Link></p>                    </form>                </div>            </div>          )      }    }}const mapStateToProps = state =>({    auth: state.auth,    isAuthenticated: state.auth.isAuthenticated,    error: state.error.msg})export default connect(mapStateToProps, mapDispatchToProps)(Login)

So we have defined two functions in this file, one mapStateToProps and one mapDispatchToProps. In the mapStateToProps function, we are mapping the states and state properties into properties of the class Login. In the mapDispatchToProps, we are creating a property, login that takes a username and a password and dispatches the login action.

11. In the Register.js file, add:

import React, { Component } from 'react'import { Link, Redirect } from 'react-router-dom';import {connect } from "react-redux";import {register} from "../../actions/auth";
const mapDispatchToProps = dispatch =>({ register: (user) => dispatch(register(user))})export class Register extends Component { state={ username: '', email: '', password: '', password2: '', } onSubmit = e =>{ e.preventDefault(); const {username, email, password, password2} = this.state; if(password != password2){ alert("Passswords must match."); } else{ const newUser = {username, email, password}; this.props.register(newUser); } } onChange = e=> this.setState({[e.target.name]: e.target.value}); render() { const {username, email,password, password2} = this.state; if(this.props.isAuthenticated){ return <Redirect to= "/"/> } else{ if(this.props.error){ return <h4>{this.props.error}: A user with that name exists. Refresh.</h4> } return ( <div className="col-md-6 m-auto"> <div className="card card-body mt-4 mb-4"> <h1>Register</h1> <form onSubmit={this.onSubmit}> <div className="form-group"> <label>Name</label> <input className="form-control" type="text" name="username" onChange = {this.onChange} value={username}/> </div> <div className="form-group"> <label>Email</label> <input className="form-control" type="email" name="email" onChange={this.onChange} value={email}/> </div> <div className="form-group"> <label>Password</label> <input className="form-control" type="password" name="password" onChange={this.onChange} value={password}/> </div> <div className="form-group"> <label>Confirm Password</label> <input className="form-control" type="password" name="password2" onChange={this.onChange} value={password2}/> </div> <div className="form-group"> <button type="submit" className="btn btn-primary"> Register </button> </div> <p>Already have an Account? <Link to="/login">Login</Link></p> </form> </div> </div> ) } }}const mapStateToProps = state =>({ isAuthenticated: state.auth.isAuthenticated, error: state.error.msg})export default connect(mapStateToProps, mapDispatchToProps)(Register)

Similar to Login.js.

12. Then within the components folder, create another folder messages, and within it create a file Form.js, and add:

import React, { Component, Fragement } from 'react'import {addMessage} from "../../actions/messages";import {connect} from "react-redux";
const mapStateToProps = state =>({ error: state.error.msg})const MapDispatchToProps = dispatch =>({ addMessage: (message)=> dispatch(addMessage(message))})export class Form extends Component { state= { name: '', email: '', message: '', } onChange = e => this.setState({ [e.target.name] : e.target.value}); onSubmit = e => {e.preventDefault(); console.log("submit"); this.props.addMessage(this.state); } render() { const {name, email, message} = this.state; if(this.props.error !== null){ alert(this.props.error); return(<div><p style={{textAlign: 'center', display: 'block'}}><h4>An error occured. Try filling up valid info in the fields.</h4></p> </div>) } else{ return (<div className="card card-body mt-4 mb-4"> <h1>Add Message</h1> <form onSubmit={this.onSubmit}> <div className="form-group"> <label>Name</label> <input className="form-control" type="text" name="name" onChange = {this.onChange} value={name}/> </div> <div className="form-group"> <label>Email</label> <input className="form-control" type="email" name="email" onChange={this.onChange} value={email}/> </div> <div className="form-group"> <label>Message</label> <input className="form-control" type="text" name="message" onChange={this.onChange} value={message}/> <div className="form-group"> <button type="submit" className="btn btn-primary"> Submit </button> </div> </div> </form> </div>); } }}export default connect(mapStateToProps,MapDispatchToProps)(Form);

13 . Then create a Messages.js file and add:

import React, { Component, Fragment } from 'react'import {connect} from "react-redux";import {getMessages, deleteMessages} from "../../actions/messages";const mapStateToProps = state => ({    messages: state.messages.messages,    error: state.messages.errMess});const MapDispatchtoProps = dispatch =>({    getMessages:() => {dispatch(getMessages())},    deleteMessages: (id)=> dispatch(deleteMessages(id)),})class Messages extends Component {    componentDidMount(){        this.props.getMessages();    }    render() {        if(this.props.error){           return(<div>                     <h2>Messages</h2>                    <table className="table table-striped">                       <thead>                           <tr>                           <th>ID</th>                           <th>Name</th>                           <th>Email</th>                           <th>Message</th>                           </tr>                      </thead>                  </table>                  <p style={{textAlign: 'center', display: 'block'}}> 
<h4>{this.props.error}: You might not be authenticated.
</h4></p></div>);
} else{ return ( <Fragment> <h2>Messages</h2> <table className="table table-striped"> <thead> <th>ID</th> <th>Name</th> <th>Email</th> <th>Message</th> </thead> <tbody> {this.props.messages.map(message=>( <tr key={message.id}> <td>{message.id}</td> <td>{message.name}</td> <td>{message.email}</td> <td>{message.message}</td> <td><button className="btn btn-danger btn-sm" onClick={()=> this.props.deleteMessages(message.id)}>Delete</button></td> </tr> ))} </tbody> </table> </Fragment> ); } }};export default connect(mapStateToProps,MapDispatchtoProps)(Messages);

14. Finally, create the Dashboard.js file:

import React, {Fragment} from 'react'import Form from "./Form";import Messages from "./Messages";function Dashboard() {    return (          <Fragment>            <Form/>            <Messages/>         </Fragment>         )}export default Dashboard;

15. And at last, within the components folder,within the App.js file, add:

import React,{Component, Fragment} from "react";import ReactDOM from 'react-dom';import {HashRouter as Router, Route, Switch, Redirect} from 'react-router-dom';import Dashboard from "./messages/Dashboard";import Login from "./accounts/Login";import Register from "./accounts/Register";import PrivateRoutes from "./common/PrivateRoutes";import {Provider} from "react-redux";import store from "../store";import {loadUser} from  "../actions/auth";
class App extends Component{ componentDidMount(){ store.dispatch(loadUser()); } render(){ return( <Provider store={store}> <Router> <Fragment> <div className="container"> <Switch> <PrivateRoutes exact path="/" component={Dashboard}/> <Route exact path="/login" component={Login}/> <Route exact path = "/register" component=
{Register}/>
</Switch> </div> </Fragment> </Router> </Provider> ); }}ReactDOM.render(<App />, document.getElementById('app'));

You can create a header file and a footer file and add them here to have a well defined structure of the application.

So, finally in the command prompt, go to the root folder, start the pipenv shell, go to the project folder and run:

npm run dev

If everything goes well, the front-end will compile perfectly.

Then in another terminal window, start the server and voila, you have a complete web application using Django. Pat yourself in the back!

So our 3 part Django Web Application series has come to an end. I hope you got to learn something and enjoy it along the way. Best of luck for the future ahead!

Happy coding!

--

--

Himraj Gogoi

A fourth year CSE undergraduate in Jorhat Engineering College, Jorhat, Assam. Passionate about programming, latest technologies.