Login in react-redux

Introduction

This tutorial showcases a simple login example that includes a login form, a saga to query the server for authentication, redux store update for user login, which will allow access to the private pages. It will also showcase proper separation of public and private pages in React applications.

This tutorial is focused in teaching how to properly add a login page to your project. It does not contain sessions nor encription. For proper encripted login with sessions please refer to the advanced tutorial here.

Login Requirements

The login flow will require creating the following:

  1. login component - contains login form, handling login related events.
  2. login container - dispatches login actions.
  3. login actions and related constants - defines email/password state update actions and login action handling saga.
  4. login saga - contacts the server to authenticate given login details.
  5. login reducer - handles dispatched login actions.
  6. private route container - maps login state to component.
  7. private route component - routes to login page if user hasn't logged in, otherwise renders received private component.

Login Flow

The login flow has two parts. First, updating the state following the changes in email/password text fields, and second, dispatching login action once the user clicks on the login button. This tutoral focuses on the login flow itself.

The login flow begins when the user presses on the login button:

  1. Following the event, the event handler will be executed, which in turn will dispatch the login action.
  2. The login action will be intercepted by the saga middleware, and the server is contacted to authenicate the login details.
  3. Following the server response, the login success or failure actions will be dispatched from the saga.
  4. The reducer will handle the success case by changing the isLogged value to true.
  5. Due to this change, React will render the components, and this time we are redirected to /, which will render the homepage instead of rending the login page again.

Login Component - Login.js

The login component does one of two things. In case the user has logged in, which is done by checking the value of isLoggedIn, the login component will redirect to /, otherwise it will render the login page. This way we ensure that the user does not login twice.

Important login component sections of code can be seen below.

In [ ]:
const Login = (props) => {
  if (props.isLoggedIn)
    return (
      <Redirect to='/'/>
    );
  else
  return (
    <MuiThemeProvider theme={defaultTheme}>
      <div className={props.classes.loginContainer}>
       .
       .
       .
      </div>
    </MuiThemeProvider>
  );
};

export default withStyles(styles)(Login);

Login Container - LoginPage.js

In the container we will map three login related variables:

  1. isLoggedIn - true in case the user has logged in, otherwise, false.
  2. logInEmail - contains the email value of the login form.
  3. logInPassword - contains the password value of the login form.

Any change in the form data will reflect the changes on the first two parameters via two event handlers:

  1. updateEmail - which will dispatch the updateEmail action.
  2. updatePassword - which will dispatch the updatePassword action.

Once the user presses on the login button the logIn action will be dispatched. This action will be intercepted by the saga middleware, which will authenticate the values by quering the server, and dispatch the success or failure actions appropriately.

In [ ]:
const mapStateToProps = (state, ownProps) => {
  return {
    isLoggedIn: state.getIn(['global', 'isLoggedIn']),
    logInEmail: state.getIn(['global', 'logInEmail']),
    logInPassword: state.getIn(['global', 'logInPassword']),
  }
};

const mapDispatchToProps = (dispatch) => {
  return {
    logIn: (email, password) => {
      dispatch(logInActions.logIn(email, password));
    },
    updateEmail: (email) => {
      dispatch(logInActions.updateEmail(email));
    },
    updatePassword: (password) => {
      dispatch(logInActions.updatePassword(password));
    },
  }
};

export default withTheme()(connect(mapStateToProps, mapDispatchToProps)(Login));

Login Saga - Sagas.js

The login saga LoginPageSagas will intercept the action and handle it as follows:

  1. The login saga will takeEvery action of type LOGIN.
  2. The saga contacts the server to authenticate the login details.
    • calls fetch with the action URL and a JSON request.
    • the request contains email/password value.
  3. If the server approves, then the server will return OK as a response.
    • then a LOGIN_SUCCESS action is dispatched.
    • otherwise LOGIN_FAILURE is dispatched.
In [ ]:
function* logIn(action) {
  console.log('login=', action);
  try {
    const res = yield call(fetch, action.url,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(action.payload)
      });

    const json = yield call([res, 'json']);
    if (json.response === 'OK')
      yield put({type: logInConstants.LOGIN_SUCCESS, payload: json.response});
    else
      yield put({type: logInConstants.LOGIN_FAILURE, payload: json.response});
  } catch (e) {
    yield put({type: logInConstants.LOGIN_FAILURE, message: e.message});
  }
}


function* LoginPageSagas() {
  yield takeEvery(logInConstants.LOGIN, logIn);
}

export default LoginPageSagas;

Login Reducer - reducers.js

The login reducers for the actions will update the state following dispatched actions for updating email and password values as well as login success actions dispatched from the saga. The code below shows the relevant part of the store and the handled actions for the login process.

In [ ]:
const initialState = fromJS({
  isLoggedIn: false,
  logInEmail: '',
  logInPassword: '',
    .
    .
    .
});

function homePageReducer(state = initialState, action) {
  switch (action.type) {
    case logInConstants.LOGIN_SUCCESS:
      return state.set('isLoggedIn', true);
    case logInConstants.UPDATE_PASSWORD:
      return state.set('logInPassword', action.payload);
    case logInConstants.UPDATE_EMAIL:
      return state.set('logInEmail', action.payload);
    .
    .
    .
    default:
      return state;
  }
};

Private Route Container - PrivateRouteContainer.js

This container maps two important variables:

  1. isLoggedIn - used by the private route component for the routing purposes.
  2. component - rendered if the use is logged in. This variable is received as props from its parent as seen at Private Router Component below.
In [ ]:
const mapStateToProps = (state, ownProps) => {
  return {
    isLoggedIn: state.getIn(['global', 'isLoggedIn']),
    component: ownProps.component,
  }
};

Private Router Component - PrivateRoute.js

This is the most important component in this tutorial. It enables correct routing in two cases:

  1. If the user is logged in, then it will display the received component, and in our case it is the HomePage. This allows the logged in user to access the private pages.
  2. If the user is not logged in, it will redirect to /login path allowing the user to login.

The code below defines the PrivateRouter component.

In [ ]:
const PrivateRoute = (props) => {
  return (
    <Route render={() => (
      props.isLoggedIn
        ? <props.component {...props} />
        : <Redirect to={{
          pathname: '/login',
          state: { from: props.location }
        }} />
    )}/>
  )
};

export default PrivateRoute;

Application Routing - index.js

To add the login to the react application we must modify the routing rules found in our main file.

Our main router now will contain public paths only, as follows:

  1. '/login' path is routed to LoginPage, which in turn will redirect to '/' or display the login form depending whether the user is logged in or not.
  2. '/' path is routed using PrivateRouteContainer that acts as a router component which receives HomePage in its props.
  3. '*' path is routed to NotFoundPage, which handles every other route.

Inside HomePage, which also acts as a router, we route all of our application's private paths.

This ensures that our application does not display private paths prior to a successful login process.

In [ ]:
const render = () => {
  ReactDOM.render(
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <Switch>
          <Route path="/login" component={LoginPage}/>
          <PrivateRouteContainer path="/" component={HomePage}/>
          <Route path="*" component={NotFoundPage}/>
        </Switch>
      </ConnectedRouter>
    </Provider>,
    document.getElementById('app')
  );
};

Final Words

This tutorial provided the basic details of the following elements:

  1. Login process flow
  2. Login authentication with the server using sagas
  3. Public and private routing using PrivateRoute component
  4. Securing private pages behind the PrivateRoute component

Created: August 1st, 2018 Updated: August 1st, 2018