« Previous Tutorial Next Tutorial »

Last time we left off with a log-in page that technically worked, but we could only see that via our developer tools. Also, this very important state change wasn’t being reflected in our Store, the place where we’re supposed to be keeping all of our application state changes. So today we’re going to wire our log-in functionality into Redux, which will let us see that the login is working by watching state changes occurring.

This one’s going to run a bit long, so let’s get going. First we’re going to create some actions. In /src/actions create a file called authentication.js. We’ll be using three action creator functions in this file. Here they are:

export const loginAttempt = () => ({ type: 'AUTHENTICATION_LOGIN_ATTEMPT' });
export const loginFailure = error => ({ type: 'AUTHENTICATION_LOGIN_FAILURE', error });
export const loginSuccess = json => ({ type: 'AUTHENTICATION_LOGIN_SUCCESS', json });

Note that the latter two functions are a little different than any action creators we’ve made so far, because they take arguments. This will allow us to pass certain data on to our state. Note also that with ES6 arrow functions, if you only have one argument, you don’t have to put parens around it. If you have two or more, or if you have none, like loginAttempt then you need the parens. It’s a little confusing but you’ll get used to it, especially if you installed ESLint for Sublime, because it will complain to you if you break convention.

We’re done with this file. Easy, right? Now let’s create a reducer to listen to those actions. In /src/reducers, create a file named … well, authentication.js. There’s a lot of filename overlap here. Fortunately if you have multiple files with the same name opened in Sublime, the tabs will tell you where they’re located.

This file needs to listen for our three actions. It also needs to define a default state, which is a little more complex than the 0 we defined as the default state for our progress reducer. So let’s start there with this code:

const initialState = {
  firstName: '',
  id: '',
  isLoggedIn: false,
  isLoggingIn: false,
  lastName: '',
  username: '',
};

We obviously don’t want to be storing a person’s password hash in our app state. We could store their email if we wanted to, but I’d prefer to only look it up on pages where it needs to be displayed or edited. We’ll find their ID handy for a variety of reasons, and the various name strings can be used to help personalize the experience (which we’ll start doing in the next tutorial). Obviously, we start with all of these things blank, since the user hasn’t logged in yet. This means that, for now, a hard page refresh will cause issues. We’ll fix that in a later tutorial. Let’s add the rest of our reducer code. Here it is:

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case 'AUTHENTICATION_LOGIN_ATTEMPT': {
      const newState = Object.assign({}, state);
      newState.isLoggingIn = true;
      return newState;
    }
    case 'AUTHENTICATION_LOGIN_FAILURE': {
      const newState = {
        firstName: '',
        id: '',
        isLoggedIn: false,
        isLoggingIn: false,
        lastName: '',
        username: '',
      };
      return newState;
    }
    case 'AUTHENTICATION_LOGIN_SUCCESS': {
      const newState = Object.assign({}, state);
      newState.firstName = action.json.firstName;
      newState.id = action.json._id;
      newState.isLoggedIn = true;
      newState.isLoggingIn = false;
      newState.lastName = action.json.lastName;
      newState.username = action.json.username;
      return newState;
    }
    default: {
      return state;
    }
  }
}

This is big and a little ugly. There are utilities such as lodash or Immutable that we could use to shorten this code a little bit, and remove the need to use Object.assign() in order to ensure that we always work with brand new objects rather than directly manipulating the state variable, but for right now I’m going with verbose code so you can see exactly what’s happening. Let’s take it case-by-case.

For a login attempt, we take the existing state and just change “isLoggingIn” to true for the period of time during which the login is being attempted (usually less than a second). This isn’t strictly necessary, but we may have uses for it in later app development.

For a login failure, we reset the state to blank. This will impact things that our app displays, eventually. Note that while our loginFailure action accepted an “error” argument, we’re not currently doing anything with that. We’ll be making some additions in a later tutorial to display error messages in the browser.

For a login success, we populate our state with data passed back from the API code we wrote a few tutorials ago. This is why we had our login routine return JSON when a login succeeds!

Save this file and let’s wire it into our Store. Open up /src/store/index.js. Below line 4, add the following code:

import AuthenticationReducer from '../reducers/authentication';

And below line 9 add this:

  authentication: AuthenticationReducer,

That’s it. We’re just importing our reducer and adding it to the combineReducers function. Save this file, and let’s move on. Next up is /src/components/account/LoginPageContainer.jsx, and this is where the bulk of our new code’s going to happen.

First off, we’re going to redirect the user back to the home page on a successful login, which will require some functionality from our router, so under line 2 add the following code:

import { Redirect } from 'react-router-dom';

After that, we need our new reducers, so under line 7, add this:

import { loginAttempt, loginSuccess, loginFailure } from '../../actions/authentication';

Now we need to connect them to the component, so scroll to the bottom of the file and find line 60:

    decrementProgressAction: decrementProgress,

Below this line, add the following three:

    loginAttemptAction: loginAttempt,
    loginFailureAction: loginFailure,
    loginSuccessAction: loginSuccess,

Good, now we can access those functions via our component’s props. So let’s head back up to line 17. It’s time to add some component state. Add a line break under 17 for padding, and then add the following code:

    // component state
    this.state = {
      redirect: false,
    };

We’ll be writing a routine soon that changes that value to true, which will be checked while the component’s re-rendering, and then used to forward a logged-in user. Next let’s get our new functions from our props. Find line 26:

    const { decrementProgressAction, incrementProgressAction } = this.props;

And, to avoid the 100-character limit from ESLint, let’s change this to a multi-line declaration, and add our new functions, like this:

    const {
      decrementProgressAction,
      incrementProgressAction,
      loginAttemptAction,
      loginFailureAction,
      loginSuccessAction,
    } = this.props;

All right, now we’re ready to modify this file to start talking to the state. For starters, let’s run our loginAttempt action. Just below line 35:

    incrementProgressAction();

Add a line for padding and then add this code:

    // register that a login attempt is being made
    loginAttemptAction();

This will change the isLoggingIn value in our Store from false to true. In order to change it back to false, we’ll need to execute either loginSuccessAction or loginFailureAction. To do this, we’re going to use the built in promises that fetch provides, which let us string .then() methods that it will execute sequentially as each one completes.

But first, we no longer want to console log our login response, so we don’t need to assign that variable. Change line 41 from:

    const loginResponse = await fetch(

to just:

    await fetch(

Our fetch block itself doesn’t really change … we’re just adding stuff to the end of it. So, and this is important, remove the semi-colon from line 53. If you don’t, your code won’t work.

Next up, below line 53, add this:

    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })

This checks fetch’s response object to make sure the login was successful (which produces a status of 200). If it was, we run a built-in method that creates json from our returned data. If not, we just return null.

Below that code, add these lines:

    .then((json) => {
      if (json) {
        loginSuccessAction(json);
        this.setState({ redirect: true });
      } else {
        loginFailureAction(new Error('Authentication Failed'));
      }
    })

This is saying “now that we either have JSON, or null, check and see which we have and then fire the appropriate action.” So if we do indeed have JSON, then we fire off loginSuccessAction, passing the user JSON along, and then we set redirect to true in our component state (we’ll do the actual redirect below). If we have null, we fire loginFailureAction with a new error message, which, again, we’ll be displaying in a later tutorial.

Last thing we need for this block is a “catch” method, which will catch any errors that fetch itself throws. We need this because if we don’t have it, the code will stop executing on an error, and our spinner decrement action will never get called, which means a spinner spinning forever. That’s bad. Here’s the code:

    .catch((error) => {
      loginFailureAction(new Error(error));
    });

This again fires loginFailureAction which in turn sets our state back to default. Note that the closing semi-colon that we deleted a minute ago shows up here, closing the fetch block.

Now delete line 72 and the padding line below it:

    console.log(loginResponse);

It’s time to take our entire render block (lines 76 to 82) and replace it with the following:

  render() {
    const { redirect } = this.state;

    if (redirect) {
      return (
        <Redirect to="/" />
      );
    }

    return (
      <div>
        <LoginPage loginFunction={this.attemptLogIn} />
      </div>
    );
  }

Do you see what’s happening here? If our component state redirect variable is true, we render a Redirect component (which in turn just alerts our router to bounce us to the homepage). If the redirect variable is not true, we render our login page as expected. Remember, we only set our redirect variable to true when a login is successful, so that user doesn’t need to be on the login page anymore.

All right, we’re ready to test. Save this file. Here’s what the whole thing should look like, now:

import React from 'react';
import 'whatwg-fetch';
import { Redirect } from 'react-router-dom';

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { incrementProgress, decrementProgress } from '../../actions/progress';
import { loginAttempt, loginSuccess, loginFailure } from '../../actions/authentication';

import LoginPage from './LoginPage';

export class LoginPageContainer extends React.Component {
  constructor(props) {
    super(props);

    // bound functions
    this.attemptLogIn = this.attemptLogIn.bind(this);

    // component state
    this.state = {
      redirect: false,
    };
  }

  async attemptLogIn(userData) {
    const {
      decrementProgressAction,
      incrementProgressAction,
      loginAttemptAction,
      loginFailureAction,
      loginSuccessAction,
    } = this.props;

    // turn on spinner
    incrementProgressAction();

    // register that a login attempt is being made
    loginAttemptAction();

    // contact login API
    await fetch(
      // where to contact
      '/api/authentication/login',
      // what to send
      {
        method: 'POST',
        body: JSON.stringify(userData),
        headers: {
          'Content-Type': 'application/json',
        },
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (json) {
        loginSuccessAction(json);
        this.setState({ redirect: true });
      } else {
        loginFailureAction(new Error('Authentication Failed'));
      }
    })
    .catch((error) => {
      loginFailureAction(new Error(error));
    });

    // turn off spinner
    decrementProgressAction();
  }

  render() {
    const { redirect } = this.state;

    if (redirect) {
      return (
        <Redirect to="/" />
      );
    }

    return (
      <div>
        <LoginPage loginFunction={this.attemptLogIn} />
      </div>
    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    incrementProgressAction: incrementProgress,
    decrementProgressAction: decrementProgress,
    loginAttemptAction: loginAttempt,
    loginFailureAction: loginFailure,
    loginSuccessAction: loginSuccess,
  }, dispatch);
}

export default connect(null, mapDispatchToProps)(LoginPageContainer);

Time to switch over to a browser. We’ve made a lot of changes, but they’re all front-end, so if your hot-reloader was running, it may have kept up with everything. It can be a little touchy. I tend to do hard-refreshes after big changes just to be sure.

Head to our login page with your developer tools open. Everything should look the same, and once again we’re going to test first with a bad login. Type in some nonsense into the login field and then hit the button. You should see four actions fire: progress increment, login attempt, login failure, and progress decrement. You can see that the failure includes our “Authentication Failure” error text. Excellent!

Now use correct login credentials. You will again see four actions fire: progress increment, login attempt, login success, and progress decrement. You’ll also be taken to the homepage. You can check the state listed in those action logs to see how it changes as these actions fire. At the end, we have the user’s data placed correctly in our Store. Awesome.

In our next tutorial, we’re going to turn that zero into a spinner, and we’re going to make our header change based on logged-in state. See you there!

« Previous Tutorial Next Tutorial »