In recent years, we can observe a massive growth in modern web application and mobile application development due to the involvement of Javascript frontend frameworks. Web and mobile applications have become more efficient and also the speed has also improved much. Today, When we have many frontend frameworks available, It's a very challenging task to choose the correct framework for the specific requirements of a business.
React.js is one of the most popular Javascript libraries that has achieved massive popularity in web and mobile application development. Today, We will learn to create an application to show the user registration and login using React-Redux. I also added an article for user registration and login app without redux, If you are completely new then I would recommend you to understand the flow of react components first.
If you're new to Redux, you can learn about them at https://redux.js.org/introduction/getting-started.
Below are the requirements for creating the user registration login app on MERN
- Node.js
- React
- MongoDB
- IDE or Text Editor
I assume that you have already available the above tools/frameworks and you are familiar with all the above that what individually actually does. Now have a look on the basic folder structure for the app.
A brief description of each package and the function it will serve
react-redux
: allows us to use Redux
with React
react-router-dom
: used for routing purposes
redux
: used to manage state between components (can be used with React
or any other view library)
redux-thunk
: middleware for Redux
that allows us to directly access the dispatch
method to make asynchronous calls from our actions
Have a look at package.json file also before we proceed:
{
"name": "time-tracker-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.3.1",
"jquery": "^3.4.1",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-redux": "^7.1.1",
"react-router-dom": "^5.0.1",
"react-scripts": "3.1.2",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"validator": "^11.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Let's Get Started
Create React App
Create a new react app using below command on terminal
create-react-app user-registration-login-app
If you have yarn package manager installed proceed accordingly.
Open localhost:3000 in your browser to see the basic react app in running.
Now we will create the needed components Register, Login, Home
Alert Action Creators
In the folder structure above we can see a folder named actions, It contains action creator related to any action inside the app.
alert.actions.ts(src/actions/action.action.js)
import { appConstants } from '../helpers';
export const alertActions = {
success,
error,
clear
};
function success(message) {
return { type: appConstants.SUCCESS, message };
}
function error(message) {
return { type: appConstants.ERROR, message };
}
function clear() {
return { type: appConstants.CLEAR };
}
In the above file, all types of alert types have been defined.
User Action Creators
user.action.js(src/actions/user.action.js)
import { userService } from '../services';
import { appConstants,history } from '../helpers';
import { alertActions } from './';
export const userActions = {
login,
logout,
register
};
function login(email, password) {
return dispatch => {
dispatch(request({
email
}));
userService.login(email, password)
.then(
user => {
dispatch(success(user));
history.push('/');
},
error => {
dispatch(failure(error));
dispatch(alertActions.error(error));
}
);
};
function request(user) {
return {
type: appConstants.LOGIN_REQUEST,
user
}
}
function success(user) {
return {
type: appConstants.LOGIN_SUCCESS,
user
}
}
function failure(error) {
return {
type: appConstants.LOGIN_FAILURE,
error
}
}
}
function logout() {
userService.logout();
return { type: appConstants.LOGOUT };
}
function register(user) {
return dispatch => {
dispatch(request(user));
userService.register(user)
.then(
user => {
dispatch(success());
// history.push('/login');
dispatch(alertActions.success('Registration successful. Login to Continue!'));
},
error => {
console.log(error,'error--------------------------')
dispatch(failure(error));
dispatch(alertActions.error(error));
}
);
};
function request(user) {
return {
type: appConstants.REGISTER_REQUEST,
user
}
}
function success(user) {
return {
type: appConstants.REGISTER_SUCCESS,
user
}
}
function failure(error) { return { type: appConstants.REGISTER_FAILURE, error } }
}
The above file, user.action.js, contains Redux action creators for actions related to users(like login, register).
index.js (src/actions/index.js)
export * from './user.actions';
export * from './alert.actions';
The above file is used just for easy access of action files in the application.
Components
Components are the building blocks of any React.js app and a typical React app will have many of these. Simply put, a component is a JavaScript class or function that optionally accepts inputs i.e. properties(props) and returns a React element that describes how a section of the UI (User Interface) should appear.
In this app we have mainly 3 components Login, Register, Home.
Register component
Register.js (src/components/auth/Register.js)
import React, { Component } from 'react';
import {Link} from 'react-router-dom';
import { connect } from 'react-redux';
import { userActions } from '../../actions';
class Register extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
firstName: '',
lastName: '',
userName:'',
submitted : false
};
this.handleInputChange = this.handleInputChange.bind(this);
this.submitRegister = this.submitRegister.bind(this);
}
handleInputChange(e) {
let name = e.target.name;
let value = e.target.value;
this.setState({
[name]: value
});
}
submitRegister(e) {
e.preventDefault();
this.setState({ submitted: true });
const { firstName, lastName, userName, email, password } = this.state;
if (email && password) {
this.props.register({firstName, lastName, userName, email, password});
}
}
render(){
const { registering } = this.props;
const { firstName, lastName, userName, email, password, submitted } = this.state;
return(
<div className="col-md-4 col-md-offset-4">
<h2 className="text-center">User Registration</h2><form>
<div className={'form-group' + (submitted && !firstName ? ' has-error' : '')}>
<label>First Name:</label>
<input type="text" id="firstName" className="form-control input-shadow" placeholder="Enter First Name" value={this.state.firstName} onChange={this.handleInputChange} name="firstName"/>
{ submitted && !firstName && <div className="help-block">First Name is required</div> }
</div>
<div className={'form-group' + (submitted && !lastName ? ' has-error' : '')}>
<label>Last Name:</label>
<input type="text" id="lastName" className="form-control input-shadow" placeholder="Enter Last Name" value={this.state.lastName} onChange={this.handleInputChange} name="lastName"/>
{ submitted && !lastName && <div className="help-block">Last Name is required</div> } </div>
<div className={'form-group' + (submitted && !userName ? ' has-error' : '')}>
<label>Username:</label>
<input type="text" id="userName" className="form-control input-shadow" placeholder="Enter user name" value={this.state.userName} onChange={this.handleInputChange} name="userName"/>
{ submitted && !userName && <div className="help-block">username is required</div> }
</div>
<div className={'form-group' + (submitted && !email ? ' has-error' : '')}>
<label>Email:</label>
<input type="text" id="email" className="form-control input-shadow" placeholder="Enter Email" value={this.state.email} onChange={this.handleInputChange} name="email"/>
{ submitted && !email && <div className="help-block">Email is required</div> }
</div>
<div className={'form-group' + (submitted && !password ? ' has-error' : '')}>
<label for="password">Password: </label>
<input type="password" id="password" className="form-control input-shadow" placeholder="Enter Password" value={this.state.password}
onChange={this.handleInputChange} name="password"/>
{ submitted && !firstName && <div className="help-block">Password is required</div> }
</div>
<button type="button" onClick={this.submitRegister} className="btn btn-primary btn-block">Register</button>
<Link to="/login" className="btn btn-link">Login</Link>
</form></div>
)
}
}
function mapState(state) {
const { registering } = state.register;
return { registering };
}
const actionCreators = {
register: userActions.register
}
const connectedRegister = connect(mapState, actionCreators)(Register);
export { connectedRegister as Register };
In the above file(Register Component) renders a simple registration form with fields, firstName, lastName, username, email, password, and a button to submit the form. The available fields are also defined in state and updating on change of values in it with the help of handleChange function. On clicking of the form button submitRegister() method is called which is defined at the top.
Login Component
Login.js (src/components/auth/Login.js)
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { userActions } from '../../actions';
class Login extends Component {
constructor(props) {
super(props);
this.props.logout();
this.state = {
email: '',
password: '',
submitted: false
};
this.handleInputChange = this.handleInputChange.bind(this);
this.submitLogin = this.submitLogin.bind(this);
}
handleInputChange(e) {
let name = e.target.name;
let value = e.target.value;
this.setState({
[name]: value
});
}
submitLogin(e) {
e.preventDefault();
this.setState({ submitted: true });
const { email, password } = this.state;
if (email && password) {
this.props.login(email, password);
}
}
render(){
const { loggingIn } = this.props;
const { email, password, submitted } = this.state;
return(
<div className="col-md-4 col-md-offset-4">
<h2 className="text-center">User Login</h2>
<form name="form">
<div className={'form-group' + (submitted && !email ? ' has-error' : '')}>
<label for="email">Email:</label>
<input type="text" id="email" className="form-control input-shadow" placeholder="Enter Email" value={this.state.email} onChange={this.handleInputChange} name="email"/>
{submitted && !email && <div className="help-block">Email is required</div> }
</div>
<div className={'form-group' + (submitted && !password ? ' has-error' : '')}>
<label>Password: </label>
<input type="password" id="exampleInputPassword" className="form-control input-shadow" placeholder="Enter Password" value={this.state.password} onChange={this.handleInputChange} name="password"/>
{submitted && !email && <div className="help-block">Password is required</div> }
</div>
<button type="button" onClick={this.submitLogin} className="btn btn-primary btn-block">Sign In</button>
<Link to="/register" className="btn btn-link">Register</Link>
</form>
</div>
)
}
}
function mapState(state) {
const { loggingIn } = state.authentication;
return { loggingIn };
}
const actionCreators = {
login: userActions.login,
logout: userActions.logout
};
const connectedLoginPage = connect(mapState, actionCreators)(Login);
export { connectedLoginPage as Login };
In the above file(Login Component) renders a simple login form with fields, email, password, and a button to submit the form. The available fields are also defined in state and updating on change of values in it with the help of handleChange function. On submit of the form button submitLogin() method is called which is defined at the top.
Home Component
Home.js (src/components/Home/Home.js)
import React, { Component} from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { userActions } from '../../actions';
class Home extends Component {
render() {
return (
<div className="text-center">
<h2>Login Successful!</h2>
<h3>Welcome to Home Page</h3>
<button className="btn btn-primary" onClick={this.getHome}>Home</button>
<Link className="dropdown-item" to="/login">Logout</Link>
</div>
)
}
}
function mapState(state) {
const { users, authentication } = state;
const { user } = authentication;
return { user, users };
}
const actionCreators = {
getUsers: userActions.getAll,
deleteUser: userActions.delete
}
const connectedHome = connect(mapState, actionCreators)(Home);
export { connectedHome as Home };
In the above file(Home Component) just showed the login success message and button to logout.
PrivateRoute Component
PrivateRoute.js (src/components/PrivateRoute.js)
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props => (
localStorage.getItem('user')
? <Component {...props} />
: <Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)} />
)
Helpers
In the project, we have used some functions/codes as a helper.
app constants (src/helpers/app-components.js)
export const appConstants = {
LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
SUCCESS: 'ALERT_SUCCESS',
ERROR: 'ALERT_ERROR',
CLEAR: 'ALERT_CLEAR',
LOGOUT: 'USERS_LOGOUT',
REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
REGISTER_FAILURE: 'USERS_REGISTER_FAILURE'
};
Auth Header(src/helpers/auth-header.js)
export function authHeader() {
// return authorization header with jwt token
let user = JSON.parse(localStorage.getItem('user'));
if (user && user.token) {
return { 'Authorization': 'Bearer ' + user.token };
} else {
return {};
}
}
The auth header is used to make authenticated HTTP requests to the server API using JWT authentication.
Form Validator (src/helpers/form-validator.js)
import validator from 'validator';
class FormValidator {
constructor(validations) {
this.validations = validations;
}
validate(state) {
let validation = this.valid();
// for each validation rule
this.validations.forEach(rule => {
if (!validation[rule.field].isInvalid) {
const field_value = state[rule.field].toString();
const args = rule.args || [];
const validation_method =
typeof rule.method === 'string' ?
validator[rule.method] :
rule.method
if (validation_method(field_value, ...args, state) !== rule.validWhen) {
validation[rule.field] = {
isInvalid: true,
message: rule.message
}
validation.isValid = false;
}
}
});
return validation;
}
valid() {
const validation = {}
this.validations.map(rule => (
validation[rule.field] = {
isInvalid: false,
message: ''
}
));
return {
isValid: true,
...validation
};
}
}
export default FormValidator;
Router History (src/helpers/history.js)
This is a custom history object userd by the React Router
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory();
Redux Store (src/helpers/store.js)
Creates a Redux store that holds the complete state tree of your app. There should only be a single store in your app.
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import rootReducer from '../reducers';
const loggerMiddleware = createLogger();
export const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);
Redux Reducers
The reducer is a pure function that takes the previous state and an action, and returns the next state. It's called a reducer because it's the type of function you would pass to Array. This folder contains all the redux reducers for the project.
Alert Reducer (src/reducers/alert.reducer.js)
This reducer manages the needed alert notifications in the application, it updates state when an alert action is dispatched from anywhere in the application.
import { appConstants } from '../helpers/app-constants';
export function alert(state = {}, action) {
switch (action.type) {
case appConstants.SUCCESS:
return {
type: 'alert-success',
message: action.message
};
case appConstants.ERROR:
return {
type: 'alert-danger',
message: action.message
};
case appConstants.CLEAR:
return {};
default:
return state
}
}
Authentication Reducer (src/reducers/auth.reducer.js)
This reducer manages the state after login and logout. on successful login, the current user object and a logged in flag are stored in the authentication
section of the application state. On logout or login failure the authentication state is set to an empty object, and during login (between login request and success/failure) the authentication state has a logging flag set to true and a user object with the details of the user that is attempting to login.
import { appConstants } from '../helpers/app-constants';
let user = JSON.parse(localStorage.getItem('user'));
const initialState = user ? { loggedIn: true, user } : {};
export function authentication(state = initialState, action) {
switch (action.type) {
case appConstants.LOGIN_REQUEST:
return {
loggingIn: true,
user: action.user
};
case appConstants.LOGIN_SUCCESS:
return {
loggedIn: true,
user: action.user
};
case appConstants.LOGIN_FAILURE:
return {};
case appConstants.LOGOUT:
return {};
default:
return state
}
}
Register Reducer (src/reducers/register.reducer.js)
This Reducer manages the registration section of the application state.
import { appConstants } from '../helpers/app-constants';
export function register(state = {}, action) {
switch (action.type) {
case appConstants.REGISTER_REQUEST:
return { registering: true };
case appConstants.REGISTER_SUCCESS:
return {};
case appConstants.REGISTER_FAILURE:
return {};
default:
return state
}
}
Services
This part mainly helps to manage all the HTTP requests to get/update the data at the server end.
User Service (src/services/user.service.js)
// import { authHeader } from '../helpers';
export const userService = {
login,
logout,
register
};
function login(email, password) {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
password
})
};
return fetch(`http://localhost:3001/auth/login`, requestOptions)
.then(handleResponse)
.then(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user));
return user;
});
}
// remove user from local storage to log user out
function logout() {
localStorage.removeItem('user');
}
// register user request
function register(user) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
};
return fetch(`http://localhost:3001/auth/register`, requestOptions).then(handleResponse);
}
function handleResponse(response) {
return response.text().then(text => {
const data = text && JSON.parse(text);
if (!response.ok) {
if (response.status === 401) {
// auto logout if 401 response returned from api
logout();
// location.reload(true);
}
const error = (data && data.message) || response.statusText;
return Promise.reject(error);
}
return data;
});
}
Update App Component (src/app.js)
import React, { Component } from 'react';
import { Router, Route } from 'react-router-dom';
import {Login} from './components/auth/Login';
import { Register } from './components/auth/Register';
import {Home} from './components/Home/Home';
import { history } from './helpers';
import { PrivateRoute } from './components/PrivateRoute'
import { connect } from 'react-redux';
import { alertActions } from './actions';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
history.listen((location, action) => {
// clear alert on location change
this.props.clearAlerts();
});
}
render() {
const { alert } = this.props;
return (
<div>
{alert.message &&
<div className={`alert ${alert.type}`}>{alert.message}</div>
}
<Router history={history}>
<div>
<PrivateRoute exact path="/" component={Home} />
<Route path="/login" component={Login} />
<Route path="/register" component={Register} />
</div>
</Router>
</div>
);
}
}
function mapState(state) {
const { alert } = state;
return { alert };
}
const actionCreators = {
clearAlerts: alertActions.clear
};
const connectedApp = connect(mapState, actionCreators)(App);
export { connectedApp as App };
Update Index File(src/index.js)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import {App} from './App';
import * as serviceWorker from './serviceWorker';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import { store } from './helpers';
// ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
Server End Task:
Please find the detailed blog on creating a server for user registration and login application Start Creating APIs in Node.js with Async/Await
Conclusion
n this demo, we understand the React-Redux User Registration and login in Reactjs using Node.js & Express.js at the server end. You can find other demos at Reactjs Sample Projects
That’s all for now. Thank you for reading and I hope this post will be very helpful for React + Redux - User Registration and Login App.
Let me know your thoughts over email demo.jsonworld@gmail.com. I would love to hear them and If you like this article, share it with your friends.
Thank You!