Login with mocked data (front-end)

We will implement login functionality that works purely on the front-end (later we will create a simple backend end point). The first step is to prepare a new session reducer, which will be app-wise (it will be used all across the app).

Create a new directory at "src/modules" location
.. and then create a new file src/modules/session.js
IMPORTANT: we are creating this module directory in the main src/** location because that one reducer needs to be available all across the app. In the session reducer we will keep all the actions and data related to our user session (like a token or login status).

931_code1

Above we have created SESSION_LOGIN_SUCCESS and SESSION_LOGIN_FAIL actions.

The interesting part is the loginAsync function which looks like what you see below:

export const loginAsync = (loginObj, push) => {
  return async (dispatch, getState) => {
    let loginToken = await loginRequest(loginObj.user, loginObj.password)

    if (loginToken !== 'invalid') {
      dispatch(loginSuccess(loginToken))
      push('/dashboard')
    } else {
      dispatch(loginFail(loginToken))
    }
  }
}

The function received the loginObj which is composed of two keys:

loginObj.user
loginObj.password

And method push, which redirects application on dashboard route when user logged in successfully.

Method loginRequest(loginObj.user, loginObj.password) is in the utils directory

New helper file at src/utils/api.js

931_code1

That data will be sent to the backend server (after we will unmock it). Currently we only check to see, if the logins are correct with this statement:

if(login === 'przeor' && password === 'mwp.io') {

(this above is just a mock, until we will implement the backend's glue mechanism later in the tutorial)

Also as you can see, we have created an asynchronous function (async (login, password) => {) which `awaits` on the promise resolve after 200 milliseconds timeout (currently it's a mock, later it will be real POST to the server). Then depending on if you have provided the correct login details, it returns:

    }).then(() => {

      if(login === 'przeor' && password === 'mwp.io') {
        return 'www.mwp.io' // just a mocked token
      } else {
        return 'invalid' // mocked non successful login
      }
    })

The 'invalid' means, that you have provided incorrect login details (later that will be sent back from the server).

The last executing code is:

    if(loginToken !== 'invalid') {
      dispatch(loginSuccess(loginToken))
      push('/dashboard')
    } else {
      dispatch(loginFail(loginToken))
    }

The dispatch comes from the react-thunk - that means, that the loginAsync is returned immediately and waits for a lazy evaluation (in our case on a response from the server, if the user has provided the correct or incorrect details) and then dispatch an action depending on the server response.

... with valid details, we dispatch two actions:

dispatch(loginSuccess(loginToken))
push('/dashboard')

dispatch is for the loginSuccess with the loginToken value as a variable and the second line is the push which comes from the parameters

continuation of src/modules/session.js

932_code2

... and above you can see that the remaining action handlers and the session reducer looks like the section below:

// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
  [SESSION_LOGIN_SUCCESS]: (state, action) => {
    return {
      ...state,
      loginToken: action.payload,
      isLoggedIn: true
    }
  },
  [SESSION_LOGIN_FAIL]: (state, action) => {
    return {
      ...state,
      loginToken: action.payload
    }
  }
}

// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
  count: 0,
  isNotLoggedIn: true,
  loginToken: 'none'
}

There is a flag for simplicity which handles if a user is logged in (isNotLoggedIn) and we keep the token in that reducer the token which shall be sent to the backend on each request while a user is logged in (the loginToken may come from the backend's JSON Web Token).

For now we are implementing a very simple login solution and then we will build upon it to make it more powerful.

Changes in (you can click the diffs image to make it larger):
src/store/reducers.js

933_code3

Above we are simply importing the session reducer with the import session from '../modules/session' and then we add it to the combineReducers:

export const makeRootReducer = (asyncReducers) => {
  return combineReducers({
    // Add sync reducers here
    router,
    session,
    ...asyncReducers
  })
}

That's all there, then we need to improve CoreLayout.js:

Changes in (you can click the diffs image to make it larger):
src/layouts/CoreLayout/CoreLayout.js

934_code4

Above, we make the CoreLayout as a smart component which is connected to the session reducer with the use of:

import React, { Component, PropTypes } from 'react'
import Header from '../../components/Header'
import './CoreLayout.scss'
import '../../styles/core.scss'
import { connect } from 'react-redux'
import { loginAsync } from '../../modules/session'

const mapActionCreators = {
  loginAsync
}

const mapStateToProps = (state) => ({
  session: state.session
})

As you know, the mapActionCreators and mapStateToProps are connected to the redux via import { connect } from 'react-redux'.

Then we improve the CoreLayout component as following:

class CoreLayout extends Component {
  static propTypes = {
    children    : PropTypes.element.isRequired,
    session     : PropTypes.object.isRequired,
    loginAsync  : PropTypes.func.isRequired
  }

  static contextTypes = {
    router: React.PropTypes.object
  }

  handleLogin = (loginObj) => {
    this.props.loginAsync(loginObj, (path) => this.context.router.push(path))
  }

  render () {
    const { children } = this.props

    return (
      <div className='container text-center'>
        <Header
          handleLogin={this.handleLogin}
          session={this.props.session} />
        <div className='core-layout__viewport'>
          {children}
        </div>
      </div>
    )
  }
}


export default connect(mapStateToProps, mapActionCreators)(CoreLayout)

In the CoreLayout you can find a function that handles login called handleLogin(the e.preventDefault() is a standard thing so I won't explain how it works here) which sends the user and password's object to the session reducer with the use of this.props.loginAsync(loginObj) and callback function, which will call for redirect on the /dashboard> url.

To the Header's component we send down the handleLogin and the session's reducer data (session={this.props.session}).

Changes in (you can click the diffs image to make it larger):
src/components/Header/Header.js

935_code5

According to login flow we have to implement the component LoginJSX (source will be below) which returns a form:

{ !session.isLoggedIn && <LoginJSX handleLogin={handleLogin} /> }

We simply return the login form in case if the user is not logged in !session.isLoggedIn and in case the user enters the wrong details then we show him a message:

let loginMessageJSX = (!session.isLoggedIn && session.loginToken === 'invalid')
    ? <p>Invalid login details, please try with correct user and password</p>
    : null

New LoginJSX component which contains a form:

New file src/components/Header/LoginJSX.js

935_code5

There are a new methods: inputOnChange and handleLogin.

First method helps (inputOnChange) to write input changing into state in the component and the second one (handleLogin) gets passed a user object (user, password fields) into our login method (from the CoreLayout). If you think it requires a more detailed description then please mail us at kamil.przeorski@gmail.com.

Everything is complete now besides the Dashboard improvements:

937_code7

Above we simply add the session reducer to the DashboardContainer and then check whether a user is logged in and if not then we show him a message via the render function:

render () {
  if(!this.props.session.isLoggedIn) {
    return <h4>Please login in order to access your dashboard</h4>
}

Based on all the changes we have made, you should be able to run this app with login required to see the dashboard as on the animation below:

938_anim

Commits screenshots source: https://github.com/ReactPoland/reactjs-redux-tutorial/commit/chapter-7

React Poland 2017