Hello friends! So, this is the sixth part of the MERN Stack series. In the first four parts, we discussed the backend part of our application in complete detail — from setting up the routes to accepting payments via stripe, we did all the backend work in those four parts. Then in the fifth part, we started building our frontend side, setting up the Redux actions and store.
Notice: I will publish the complete detailed version of all the articles on the Medium website. Here I will give an overview and give the codes for the various pages part by part.
So, please click here to go to Medium and read it in completion. (These are friend links so do not worry about paywall)
So, let’s start to build the reducer files first. We would create a folder inside the client folder which we would name as reducers. Inside this folder, we would create six files — index, authReducer, itemReducer, errorReducer, cartReducer and orderReducer.
authReducer
import {
USER_LOADING,
USER_LOADED,
AUTH_ERROR,
LOGIN_SUCCESS,
LOGIN_FAIL,
LOGOUT_SUCCESS,
REGISTER_SUCCESS,
REGISTER_FAIL
} from '../actions/types';
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
isLoading: false,
user: null
}
export default function(state=initialState, 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 AUTH_ERROR:
case LOGIN_FAIL:
case LOGOUT_SUCCESS:
case REGISTER_FAIL:
localStorage.removeItem('token');
return{
...state,
token: null,
user: null,
isAuthenticated: false,
isLoading: false
};
default:
return state;
}
}
itemReducer
import { GET_ITEMS, ADD_ITEM, UPDATE_ITEM, DELETE_ITEM, ITEMS_LOADING } from '../actions/types';
const initialState = {
items: [],
loading: false
}
export default function(state=initialState, action){
switch(action.type){
case GET_ITEMS:
return{
...state,
items: action.payload,
loading: false
}
case ADD_ITEM:
return{
...state,
items: [action.payload, ...state.items]
}
case DELETE_ITEM:
return{
...state,
items: state.items.filter(item => item._id!==action.payload)
};
case UPDATE_ITEM:
const { id, data } = action.payload;
return{
...state,
items: state.items.map(item => {
if(item._id===id){
item = data;
}
})
}
case ITEMS_LOADING:
return{
...state,
loading: true
}
default:
return state;
}
}
errorReducer
import { GET_ERRORS, CLEAR_ERRORS } from '../actions/types';
const initialState = {
msg: {},
status: null,
id: null
}
export default function(state=initialState, action){
switch(action.type){
case GET_ERRORS:
return{
msg: action.payload.msg,
status: action.payload.status,
id: action.payload.id
};
case CLEAR_ERRORS:
return{
msg: {},
status: null,
id: null
};
default:
return state;
}
}
cartReducer
import { GET_CART, ADD_TO_CART, DELETE_FROM_CART, CART_LOADING } from '../actions/types';
const initialState = {
cart: null,
loading: false
}
export default function(state=initialState, action){
switch(action.type){
case GET_CART:
return {
...state,
cart: action.payload,
loading: false
}
case ADD_TO_CART:
return {
...state,
cart: action.payload
}
case DELETE_FROM_CART:
return {
...state,
cart: action.payload
}
case CART_LOADING:
return {
...state,
loading: true
}
default:
return state;
}
}
orderReducer
import { GET_ORDERS, CHECKOUT, ORDERS_LOADING } from '../actions/types';
const initialState = {
orders: [],
loading: false
}
export default function(state=initialState, action){
switch(action.type){
case GET_ORDERS:
return{
...state,
orders: action.payload,
loading: false
}
case CHECKOUT:
return{
...state,
orders: [action.payload, ...state.orders]
}
case ORDERS_LOADING:
return{
...state,
loading: true
}
default:
return state;
}
}
index (Combining Reducers)
import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import errorReducer from './errorReducer';
import authReducer from './authReducer';
import cartReducer from './cartReducer';
import orderReducer from './orderReducer';
export default combineReducers({
item: itemReducer,
error: errorReducer,
auth: authReducer,
cart: cartReducer,
order: orderReducer
})
Now, we have finished all the redux work. Now, we can focus on building components for our application.
So, we will deal with the components needed for authentication in this part and will deal with all the other components in the next part of the series.
First of all, we would create a folder called components inside the client folder. Since we will keep all the authentication components separate from the rest of our components, so will make another folder named auth inside the components folder.
Both of our Login and Register will be modal based which would be displayed in the Navbar.
loginModal
import { Component } from 'react';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input,
NavLink,
Alert
} from 'reactstrap';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { login } from '../../actions/authActions';
import { clearErrors } from '../../actions/errorActions';
class LoginModal extends Component {
state = {
modal: false,
email: '',
password: '',
msg: null
};
static propTypes = {
isAuthenticated: PropTypes.bool,
error: PropTypes.object.isRequired,
login: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired
}
componentDidUpdate(prevProps) {
const { error, isAuthenticated } = this.props;
if(error !== prevProps.error){
// Check for login error
if(error.id === 'LOGIN_FAIL'){
this.setState({msg: error.msg.msg});
}
else{
this.setState({msg:null});
}
}
// If authenticated, close modal
if(this.state.modal){
if(isAuthenticated){
this.toggle();
}
}
}
toggle = () => {
// Clear errors
this.props.clearErrors();
this.setState({
modal: !this.state.modal
});
}
onChange = (e) => {
this.setState({[e.target.name]:e.target.value});
}
onSubmit = (e) => {
e.preventDefault();
const {email, password} = this.state;
const user = {email, password};
// Attempt to login
this.props.login(user);
}
render(){
return(
<div className="container">
<Button color="success" className="btn btn-sm"><NavLink onClick={this.toggle} href="#"><span className="text-dark"><b>Login</b></span></NavLink></Button>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
>
<ModalHeader toggle={this.toggle}>
Login
</ModalHeader>
<ModalBody>
{this.state.msg ? (<Alert color="danger">{this.state.msg}</Alert>):null}
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="email">Email</Label>
<Input
type="email"
name="email"
id="email"
placeholder="Email"
className="mb-3"
onChange={this.onChange}
/>
<Label for="password">Password</Label>
<Input
type="password"
name="password"
id="password"
placeholder="Password"
className="mb-3"
onChange={this.onChange}
/>
<Button
color="dark"
style={{marginTop: '2rem'}}
block
>Login</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated,
error: state.error
});
export default connect(mapStateToProps,{login, clearErrors})(LoginModal);
registerModal
import { Component } from 'react';
import {
Button,
Modal,
ModalHeader,
ModalBody,
Form,
FormGroup,
Label,
Input,
NavLink,
Alert
} from 'reactstrap';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { register } from '../../actions/authActions';
import { clearErrors } from '../../actions/errorActions';
class RegisterModal extends Component {
state = {
modal: false,
name: '',
email: '',
password: '',
msg: null
};
static propTypes = {
isAuthenticated: PropTypes.bool,
error: PropTypes.object.isRequired,
register: PropTypes.func.isRequired,
clearErrors: PropTypes.func.isRequired
}
componentDidUpdate(prevProps) {
const { error, isAuthenticated } = this.props;
if(error !== prevProps.error){
// Check for register error
if(error.id === 'REGISTER_FAIL'){
this.setState({msg: error.msg.msg});
}
else{
this.setState({msg:null});
}
}
// If authenticated, close modal
if(this.state.modal){
if(isAuthenticated){
this.toggle();
}
}
}
toggle = () => {
// Clear errors
this.props.clearErrors();
this.setState({
modal: !this.state.modal
});
}
onChange = (e) => {
this.setState({[e.target.name]:e.target.value});
}
onSubmit = (e) => {
e.preventDefault();
const { name, email, password } = this.state;
// Crete user object
const newUser = { name, email, password};
// Attempt to register
this.props.register(newUser);
}
render(){
return(
<div className="container">
<Button color="info" className="btn btn-sm"><NavLink onClick={this.toggle} href="#"><span className="text-dark"><b>Register</b></span></NavLink></Button>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
>
<ModalHeader toggle={this.toggle}>
Register
</ModalHeader>
<ModalBody>
{this.state.msg ? (<Alert color="danger">{this.state.msg}</Alert>):null}
<Form onSubmit={this.onSubmit}>
<FormGroup>
<Label for="name">Name</Label>
<Input
type="text"
name="name"
id="name"
placeholder="Name"
className="mb-3"
onChange={this.onChange}
/>
<Label for="email">Email</Label>
<Input
type="email"
name="email"
id="email"
placeholder="Email"
className="mb-3"
onChange={this.onChange}
/>
<Label for="password">Password</Label>
<Input
type="password"
name="password"
id="password"
placeholder="Password"
className="mb-3"
onChange={this.onChange}
/>
<Button
color="dark"
style={{marginTop: '2rem'}}
block
>Register</Button>
</FormGroup>
</Form>
</ModalBody>
</Modal>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated,
error: state.error
});
export default connect(mapStateToProps,{register, clearErrors})(RegisterModal);
Logout
import { Component, Fragment } from 'react';
import { logout } from '../../actions/authActions';
import { connect } from 'react-redux';
import {NavLink, Button} from 'reactstrap';
import PropTypes from 'prop-types';
export class Logout extends Component {
static propTypes = {
logout: PropTypes.func.isRequired
}
render() {
return (
<div>
<Fragment>
<Button color="danger" className="btn btn-sm"><NavLink onClick={this.props.logout} href="#"><span className="text-light"><b>Logout</b></span></NavLink></Button>
</Fragment>
</div>
)
}
}
export default connect(null,{logout})(Logout);
So, this is all for this part. We have finished off the Redux part and also built out the components needed for the authentication.
In the next part which will be the final one, we will be building the rest of the components and will finish off this series. I hope it helped you learn something and was exciting for you.
To read the complete tutorial, please move to Medium and read the complete article.
Top comments (6)
Fe part can be updated with functional components and hooks. Form validations can be used with the help of formik and types via typescript.
Thanks for your suggestions but since this is for beginners, I wanted to keep this simple and did not want to include more complext things. These things can be very well implemented to make it better.
Well, I am not quite sure if class based components are simpler as well. Thanks for the tutorial and effort anyway!
Yes, you are right. Class based comps are not any simpler than functional ones. I was talking about those validations and typescript stuff.
I too like the functional components more but this project was made that way by me so I just used class hased comps in the tutorials.
Brother you can use
just after backticks for code snippets. It will highlight your code.
Thanks for this suggestion. I will try it!