Content

  • Add Redux to React Application
  • Implementing Signup
  • Implementing Sign in
  • Add IE Support
  • Implementing Sign out

 

Add Redux to React Application

Redux is a predictable state container for JavaScript apps. From the very beginning, we need to stress that Redux has no relation to React. You can write Redux apps with React, Angular, Ember, jQuery, or vanilla JavaScript.

Redux is used mostly for application state management. To summarize it, Redux maintains the state of an entire application in a single immutable state tree (object), which can’t be changed directly. When something changes, a new object is created (using actions and reducers).

Let add required packages to our project

npm install redux --save
npm install react-redux –save
npm install redux-thunk

Next, create primary folders for a react redux application

cd src
mkdir actions
mkdir reducers

Next, we should create our Combine reducer’s function.  Combine reducer takes a hash of reducers and returns a single reducer. The resulting reducer calls every child reducer and gathers their results into a single state object. The state produced by combineReducers() namespaces the states of each reducer under their keys as passed to combineReducers()

We normally put our combineReducers() function in index.js file under reducers folder.

cd reducers
echo.>Index.js

our combineReduce()  function has only redux-form reducer now but we will add our reducers in our next steps.

import { combineReducers } from 'redux';
const rootReducer = combineReducers({
form
});

export default rootReducer;

Now that we setup Reducers, let add it to our application’s entry point. So open “Index.js” file and modify it as below:

import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { BrowserRouter as Router, Route, Link,Switch } from "react-router-dom";
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import reduxThunk from 'redux-thunk';

import App from './components/App';
import Signup from './components/auth/Signup';
import Signin from './components/auth/Signin';

import reducers from './reducers/index';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}> 
    <Router>
      <div>
          <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/Signin">Signin</Link>
          </li>
          <li>
            <Link to="/Signup">Signup</Link>
          </li>
        </ul>

        <hr />
        <Route exact path="/" component={App} />  
        <Route path="/Signin" component={Signin} />                                             
        <Route path="/Signup" component={Signup} />   
          
      </div>              
  </Router>
  </Provider>
  ,
  document.getElementById('root')
);

// Allow Hot Module Replacement
if (module.hot) {
  module.hot.accept();
}

Now if you check your application it must run as previously.

Implementing Signup

For adding signup to our react-redux application, we need to create 4 files

  • Signup Component (We already create it but it only displays a text instead of signup form)
  • Signup Action creators
  • Action Types file
  • Signup Reducer

Let first create our action types and action creator:

Create a new file named Types.js under actions folder.

cd actions
echo.>Types.js

we need 3 action types for register start, succeed and fail.

export const REGISTER_USER_START = 'register_user_start';
export const REGISTER_USER_SUCCESS = 'register_user_success';
export const REGISTER_USER_FAIL = 'register_user_fail';

Next create AuthActions.js file

cd actions
echo.>AuthActions.js

Our first action is registerUser. This action will call our dotnet application’s Register API we already wrote in the previous section, so we need to install axios (or any other package) to be able to call this API.

npm install axios

Next, let write our register action

import {
  REGISTER_USER_START,
  REGISTER_USER_FAIL,
  REGISTER_USER_SUCCESS,
} from './Types';
import axios from 'axios';
const AUTH_SERVER_URL = '';

export const registerUser = ({email, password, confirmPassword}, callback) => {
  
  return async (dispatch) => {

    dispatch({
      type: REGISTER_USER_START,
    })

    var params = {
      email, password,confirmPassword
    };

    try {
      const response = await axios.post(`${AUTH_SERVER_URL}/api/auth/register`, params)
      
      dispatch({
        type: REGISTER_USER_SUCCESS,
      })

      if(callback)
        callback();

    } catch (error) {
        let errorMessages = [];
        if(error.response.data.general) {
          Array.prototype.push.apply(errorMessages, error.response.data.general);
        }
        else {
          errorMessages.push('Registration Failed. Something went wrong!')
        }
        
        dispatch({ 
          type: REGISTER_USER_FAIL, 
          payload: errorMessages
          });
    }      
  }
}

next create our authentication reducer

cd reducers
echo.>AuthReducer.js

There is nothing specific we need to do in our reducer for now. Our Authentication State needs only one property to show the registration error. So we only prepare our “AuthReducer” to receive required action types for future enhancements.

import { 
  REGISTER_USER_START,
  REGISTER_USER_FAIL,
  REGISTER_USER_SUCCESS
 } from "../actions/Types";

 const INITIAL_STATE = {
   register_error: '',
   isLoading: false

 }

 export default(state = INITIAL_STATE, action) => {
   switch(action.type) {
     case REGISTER_USER_START:
     return { ...state, isLoading: true, register_error: ''};
     case REGISTER_USER_SUCCESS:
     return { ...state, isLoading: false, register_error: ''};
     case REGISTER_USER_FAIL:
     return { ...state, isLoading: false, register_error: action.payload};
     default: return state;
   }
 }

We should add this reducer to combineReducers function so open reducers\index.js file and modify it as below.

import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
import AuthReducer from './AuthReducer';
const rootReducer = combineReducers({
auth: AuthReducer
});

export default rootReducer;

For creating our signup form we should install redux-form package to our project.

npm install redux-form

we also need to add redux-form reducer to combineReducers function

import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
import AuthReducer from './AuthReducer';
const rootReducer = combineReducers({
form,
auth: AuthReducer,
});

export default rootReducer;

Let first create our component using redux-form and register a new user on form submit. After completing submit we will navigate the user to login page. To do this we will add “withRouter” to our component, so you will be able to programmatically navigate to another page after submitting the form.

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import { registerUser } from "../../actions/AuthActions";

class Signup extends Component {
  constructor(props) {
    super(props);
    
  }

  handleFormSubmit(data) {
     this.props.registerUser(data, this.onSubmitComplete.bind(this));
  }
 
  onSubmitComplete() {
    console.log('register succeed');
    this.props.history.push("/signin");
  }
  

  render() {
    const { handleSubmit } = this.props;
    console.log(this.props.auth.isLoading);
    
    return (
      <form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>

        <h1>Sign up</h1>
        <br/>
        <div>
          <label>email</label>
          <div>
            <Field
              name="email"
              component="input"
              type="email"
              placeholder="Email"
            />
          </div>
        </div>
        <div>
          <label>Password</label>
          <div>
            <Field
              name="password"
              component="input"
              type="password"
              placeholder="Password"
            />
          </div>
        </div>
        <div>
          <label>Confirm Password</label>
          <div>
            <Field
              name="confirmPassword"
              component="input"
              type="password"
              placeholder="Confirm Password"
            />
          </div>
        </div>
        <div>
          <span style={{color: 'red'}}>
            {this.props.auth.register_error}
          </span>
        </div>
        <div>
          <button type="submit" disabled={this.props.auth.isLoading ? 'disabled' : ''}>
            Signup
          </button>
        </div>
      </form>
    )
  }
}

Signup = reduxForm({
  form: 'signup' // a unique identifier for this form
})(Signup);

function mapStateToProp(state) {
  return {
    auth: state.auth
  };
}

Signup = connect(mapStateToProp, { registerUser })(withRouter(Signup));

export default Signup;

Now run the application, navigate to signup page and try to submit it. Below error will occur :

Uncaught ReferenceError: regeneratorRuntime is not defined

This error occurs because we wrote async actions and babel does know how to translate it. To solve this problem we should add below packages:

npm install @babel/plugin-transform-runtime --save-dev
npm install --save @babel/runtime

Open .babelrc file and add this package as a plugin :

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [  
    ["@babel/plugin-transform-runtime"]
  ]
}

For more information about this babel plugin check https://babeljs.io/docs/en/babel-plugin-transform-runtime.

Now run the application once more, you should be able to submit new user and check email and password in the console of the browser.

Implementing Sign in

We need 3 action types for login to succeed and login failed and fetch token.

export const LOGIN_USER_START = 'login_user_start';
export const LOGIN_USER_SUCCESS = 'login_user_success';
export const LOGIN_USER_FAIL = 'login_user_fail';
export const FETCH_TOKEN = 'fetch_token';

Add these types to “Authactions.js” file:

import {
  REGISTER_USER_START,
  REGISTER_USER_FAIL,
  REGISTER_USER_SUCCESS,
  LOGIN_USER_START,
  LOGIN_USER_SUCCESS,
  LOGIN_USER_FAIL,
  FETCH_TOKEN,
} from './Types';

Our next action is loginUser. This action will call /connect/token API that we already wrote in the previous section.

npm i jwt-decode

Import jwt decode to authAction.js file and write loginuser action as below:

import jwt_decode from 'jwt-decode';
.
.
.
export const loginUser = ({email, password}, callback) => {
 
   return async (dispatch) => {
    dispatch({
      type: LOGIN_USER_START,
    })
    //OpenIddict post parameters should be string as below (Json not allowed)
    var params = `username=${email}&password=${password}&grant_type=password&scope=openid offline_access profile`;
    
    try {
      const response = await axios.post(`${AUTH_SERVER_URL}/connect/token`
      , encodeURI(params), header );
      //Save Tokens local storage.
      localStorage.setItem('accessToken', response.data.access_token);      
      localStorage.setItem('refreshToken', response.data.refresh_token);
      localStorage.setItem('idToken', response.data.id_token);
      
      const now = new Date();
      let expirationDate = new Date(now.getTime() + response.data.expires_in * 1000)
                            .getTime().toString();
      localStorage.setItem('expiresAt', expirationDate);
      
      dispatch({
        type: LOGIN_USER_SUCCESS,
        });
        
      dispatch(
        {
            type: FETCH_TOKEN, 
            payload: getUser(response.data.id_token)
        });

        if(callback)
          callback();

    } catch (arg) {
      let errorMessage = "Something went wrong";
                               
      if(arg.response && arg.response.data) {
        errorMessage = arg.response.data.error_description;
      }

      dispatch({ 
        type: LOGIN_USER_FAIL, 
        payload: errorMessage
        });
    }
  }
}

const getUser = (idToken) => {

  let user = {
    username:'***'
  };

  if (idToken) {
    var decoded = jwt_decode(idToken);
    user.username = decoded.name;
  }

  return user;

}

Next step is to add sign in to “authreducer”. First, we should update our initial state by adding few properties.

  1. User : to keep login user’s info
  2. isLogin: to determine if user is logged in or not (actually it is 3 state option. Null value means that we are checking login state.
  3. Login_error: to display error message of the login form.

After preparing the initial state, we should create our action creators as below:

import { 
  REGISTER_USER_FAIL,
  REGISTER_USER_SUCCESS,
  LOGIN_USER_START,
  FETCH_TOKEN,
  LOGIN_USER_FAIL,
  LOGIN_USER_SUCCESS
 } from "../actions/Types";

 const INITIAL_STATE = {
   register_error: '',
   user: null,
   isLogin: null,
   login_error: ''
 }

 export default(state = INITIAL_STATE, action) => {
   switch(action.type) {
     case REGISTER_USER_START:
        return { ...state, isLoading: true, register_error: ''};
      case REGISTER_USER_SUCCESS:
        return { ...state, isLoading: false, register_error: ''};
      case REGISTER_USER_FAIL:
        return { ...state, isLoading: false, register_error: action.payload};
     case LOGIN_USER_START:
     return { ...state, isLoading: true, login_error: ''};
     case  FETCH_TOKEN:     
            return { ...state,           
              isLogin: true,
              user: action.payload
             };
     case LOGIN_USER_SUCCESS:
            return { ...state, isLoading: false, isLogin:true, login_error: ''};
        case LOGIN_USER_FAIL:      
            return { ...state, login_error:action.payload , isLoading: false, isLogin: false, password:'' }
     default: return state;
   }
 }

Let first create our component using redux-form and try to log form data when user clicks on submit button.

import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import { loginUser } from "../../actions/AuthActions";

class Signin extends Component {
  constructor(props) {
    super(props);
  }

  handleFormSubmit(data) {
    this.props.loginUser(data, this.onSigninComplete.bind(this));
 }

 onSigninComplete() {
   console.log('sigin succeed');
   this.props.history.push("/");
 }

  render() {
    const { handleSubmit } = this.props
    return (
      <form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
   
        <div>
          <label>email</label>
          <div>
            <Field
              name="email"
              component="input"
              type="email"
              placeholder="Email"
            />
          </div>
        </div>
        <div>
          <label>Password</label>
          <div>
            <Field
              name="password"
              component="input"
              type="password"
              placeholder="Password"
            />
          </div>
        </div>
        <div>
          <span style={{color: 'red'}}>
            {this.props.auth.login_error}
          </span>
        </div>
        <div>
          <button type="submit">
            Signin
          </button>
        </div>
      </form>
    )
  }
}

Signin = reduxForm({
  form: 'Signin' // a unique identifier for this form
})(Signin);

function mapStateToProp(state) {
  return {
    auth: state.auth
  };
}

Signin = connect(mapStateToProp, { loginUser })(withRouter(Signin));

export default Signin;

Now run your application and try to register a user and then login in with it.

Next, we will try to restrict unauthorized users to access the feature page. For this purpose, we will create a base component to load authentication needed components in it. This component is going to check if the user is logged in (isLogin = true), and then display its child component to the user. Otherwise (isLogin = false) it will redirect the user to login page. We also going to handle delay that will occur when our application is checking the authentication state (isLogin = null).

Let’s create “RequiredAuth” component:

…> cd src/components
…/src/components> echo.> RequiredAuth.js

And modify it as below:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from "react-router-dom";

export default function(ComposedComponent) {
  class Authentication extends Component {
    static contextTypes = {
      router: PropTypes.object
    }

    componentWillMount() {
      const {isLogin} = this.props.auth;

      
      if (isLogin != null && !isLogin) {
        this.props.history.push('/signin');
      }
    }

    componentWillReceiveProps(nextProps) {

      if (nextProps.auth.isLogin === false) {
        this.props.history.push('/signin');
      }
    }

    render() {
      const {isLogin} = this.props.auth;
      return isLogin != null ? (<ComposedComponent {...this.props} />)
      : (<div style={{
        display: 'flex',
        flex: 1,
        height: '100%',
        flexDirection:'column',
        alignItems:'center',
        justifyContent:'center'
      }}> 
      <h3 style={{color:'rgba(0, 0, 0, 0.54)', marginTop:20}}>Authorizing...</h3>
      </div>)
    }
  }

  function mapStateToProps(state) {
    return { auth: state.auth };
  }

  return withRouter(connect(mapStateToProps)(Authentication));
}

Next, we should add this component to index.js file and change Feature route as below:

import RequiredAuth from './components/RequiredAuth';
.
.
.
<Route path="/Feature" component={RequiredAuth(Feature)} />
.
.

Now if we run the project, below error will be shown:

… Support for the experimental syntax 'classProperties' isn't currently enabled. Add @babel/plugin-proposal-class-properties (https://git.io/vb4SL) to the 'plugins' section of your Babel config to enable transformation.
To fix this let first add @babel/plugin-proposal-class-properties package to project:

npm install @babel/plugin-proposal-class-properties

Then modify .babelrc file as below :

{
  "presets": [["es2015", { "modules": false }], "react", "stage-2"],
  "plugins": [
    ["@babel/plugin-transform-runtime"],
    "@babel/plugin-proposal-class-properties"
  ]
}

Now run the project once again and try to navigate to feature page, you should see “Authorizing…” message. Go to the login page and try to login with your user then navigate to Feature one more time, now you will see “This is Feature.js. Only authenticated Users allowed.” Message.

Just one thing left to do, we should automatically authenticate users that already get the token. For this purpose first, we need an authentication action to load things related to authentication. So open authAction.js file and add below:

export function loadThings() {
  return (dispatch, getState) => {

      
      const { auth, isLoading } = getState();

      if(auth.isLogin == null) {
        dispatch({ type: LOAD_FETCHING, fetching: true })
  
          let accessToken = localStorage.getItem('accessToken');       
          let expiresAt = localStorage.getItem('expiresAt');   

            
            if (!isExpired(expiresAt)) {
              let idToken = localStorage.getItem('idToken');           
                dispatch(
                  {
                    type: FETCH_TOKEN, payload: getUser(idToken)
                  }
                )              
            }
            else {
              
              refreshToken(dispatch);
            }        
      }
  };
}

const refreshToken = async (dispatch) => {
let refreshToken = localStorage.getItem('refreshToken');       
  
if(refreshToken) {

  var params = `refresh_token=${refreshToken}&grant_type=refresh_token&scope=openid offline_access fullname profile`;
  try {
    let response = await axios.post(`${AUTH_SERVER_URL}connect/token`, encodeURI(params), header);
    localStorage.setItem('accessToken', response.data.access_token);
    const now = new Date();
    let expirationDate = new Date(now.getTime() + response.data.expires_in * 1000).getTime().toString();

    localStorage.setItem('refreshToken', response.data.refresh_token);
    localStorage.setItem('expiresAt', expirationDate);
    localStorage.setItem('idToken', response.data.id_token);
    
    dispatch(
      {
        type: FETCH_TOKEN, 
        payload: user: getUser(response.data.id_token)
      }
    )

  } catch (error) {
    
        dispatch({
          type: LOGIN_USER_FAIL
        })
  }

     
  }
  else {
    dispatch({
      type: LOGIN_USER_FAIL
    })
  }

}

const isExpired = (expiresAt) => {
  
  if (Date.now() >= expiresAt) {
      return true;
  }
  else {
    return false;
  }
}

Need a component to check authentication token before other components are shown to users. First create startup component:

…> cd src/components
…/src/components> echo.> Startup.js

Modify this file as below:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { loadThings } from './actions/AuthActions';
import { withRouter } from "react-router-dom";

function mapStateToProps(state) {
    return {
        
    };
}

class Startup extends Component {

    componentDidMount() {        
        this.props.loadThings();
    }

    render() {
    
        return (
            <div>
                {this.props.children}
            </div>
        );
    }
}

export default withRouter(connect(
    mapStateToProps, {loadThings}
)(Startup));

Now add this component in index.js file.

import Startup from './Startup';
…   
<Startup>
      <div>
          <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/Signin">Signin</Link>
          </li>
          <li>
            <Link to="/Signup">Signup</Link>
          </li>
          <li>
            <Link to="/Feature">Feature</Link>
          </li>
        </ul>

        <hr />
        <Route exact path="/" component={App} />  
        <Route path="/Signin" component={Signin} />                                             
        <Route path="/Signup" component={Signup} />   
        <Route path="/Feature" component={RequiredAuth(Feature)} />
      </div>      
      </Startup>

If we run the application, it will check the token at startup and if no token found, the application will redirect us to the login page. Otherwise, if it found a valid token, we will be navigated to the home page.

Add IE Support

To be able to run our application in IE 11 and below, we need to add polyfills to our app. First, add a polyfill of ES6 Promise:

npm install es6-promise

Then create a folder named “Polyfills” under “src” folder and create “index.js” file in this folder:

…/src>mkdir polyfills
…/src>cd polyfills
…/src/polyfills> echo.> index.js

Add startWith() and assign() polyfills to this file:

export function startsWith() {
  if (!String.prototype.startsWith) {
    String.prototype.startsWith = function(searchString, position){
      position = position || 0;
      return this.substr(position, searchString.length) === searchString;
  };
  }
}

export function assign() {
  if (typeof Object.assign != 'function') {
    // Must be writable: true, enumerable: false, configurable: true
    Object.defineProperty(Object, "assign", {
      value: function assign(target, varArgs) { // .length of function is 2
        'use strict';
        if (target == null) { // TypeError if undefined or null
          throw new TypeError('Cannot convert undefined or null to object');
        }
  
        var to = Object(target);
  
        for (var index = 1; index < arguments.length; index++) {
          var nextSource = arguments[index];
  
          if (nextSource != null) { // Skip over if undefined or null
            for (var nextKey in nextSource) {
              // Avoid bugs when hasOwnProperty is shadowed
              if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
                to[nextKey] = nextSource[nextKey];
              }
            }
          }
        }
        return to;
      },
      writable: true,
      configurable: true
    });
  }
}

We may add any other polyfill to this file later.

Import these polyfills to “…/src/index.js” file as below:

import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { BrowserRouter as Router, Route, Link,Switch } from "react-router-dom";
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import reduxThunk from 'redux-thunk';

//Polyfills for IE
require('es6-promise').polyfill();
require('./Polyfill/index').startsWith();
require('./Polyfill/index').assign();

import App from './components/App';

Open .babelrc file and modify it as below:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"],
  "plugins": [
    ["@babel/plugin-transform-runtime"
      ,{
      "corejs": false,
      "helpers": true,
      "regenerator": true,
      "useESModules": false
    }],
    "@babel/plugin-proposal-class-properties"
  ]
}

Now if you run the application in Internet Explorer, it should be performed as expected.

Implementing Sign out

Our authentication components will be completed by adding a sign-out to it. So first let’s create our action type. Open Types.js file and add below:

export const LOGOUT_USER = 'logout_user';

Then open AuthActions.js file and add userSignout() to it:

import {
  …
  LOGOUT_USER
} from './Types';
 
export const userLogout = () => {

  return (dispatch) => {

    localStorage.removeItem('expiresAt');
    localStorage.removeItem('refreshToken');
    localStorage.removeItem('accessToken');
  
      dispatch({ type: LOGOUT_USER });
  }
}

Next, open authreducer.js file and add below:

import { 
  …,
  LOGOUT_USER
 } from "../actions/Types";
.
.
export default(state = INITIAL_STATE, action) => {
   switch(action.type) {
.	
.
.
      case LOGOUT_USER:          
        return { ...INITIAL_STATE, isLogin: false};
        break;
      default: return state;
   }
}

Create “signout” component under “auth” folder:

…/src/components/auth> echo.> signout.js

Then modify it as below:

import React, { Component} from "react";
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import { userLogout } from "../../actions/AuthActions";

class Signout extends Component {
  
  constructor(props) {
    super(props);
    
  }
  onSignoutClicked = () => {
    this.props.userLogout();
  }
  
  render()
  {
    return (
      <div>
        <p>Press below button to signout.</p>
        <button 
        onClick={this.onSignoutClicked}
        >Sign Out</button>
      </div>
    );
  }
}

Signout = connect(null, { userLogout })(withRouter(Signout));

export default Signout;

add Signout to index component :

…
import Signout from './components/auth/Signout';
…

ReactDOM.render(
  <Provider store={createStoreWithMiddleware(reducers)}> 
    <Router>
      <Startup>
        <div>
            <ul>
            …
            <li>
              <Link to="/Signout">Signout</Link>
            </li>
          </ul>

          <hr />
          …
          <Route path="/Signout" component={RequiredAuth(Signout)} />
        </div>      
      </Startup>        
  </Router>
  </Provider>
 …

There a few other features to complete our components like “forget password” and “change password” that we will not cover in this article but you may find them in our completed project on GitHub (https://github.com/imnapo/cn-react-dotnetcore).