« Previous Tutorial Next Tutorial »

It’s time to talk about the facts of life. Specifically, the life of your React components. We’re going to gently introduce the concept of component lifecycle functions in this tutorial, and we’re going to do it while fixing that problem where a hard-refresh appears to kill the user session (it doesn’t, but our React app loses track of it).

To start, we need to add a function to our API that checks for an existing session. We’ll call this function once, when our app template mounts, and that way if there’s an existing Express session, we can populate our user object without having to perform another login.

Jump into sublime and open up /api/authentication.js. I’ve been lax about keeping things alphabetical, here, so let’s reorder it quickly. I like to do GETs first, then POSTs. Change around your blocks so the GET to /logout comes first, then the POST to /login, then the POST to /register. Hooray, organization!

Now, above the GET to /logout (which should be line 11), let’s add the following code:

// GET to /checksession
router.get('/checksession', (req, res) => {
  if (req.user) {
    return res.send(JSON.stringify(req.user));
  }
  return res.send(JSON.stringify({}));
});

This is a simple routine. Once you log in via Passport, if it’s properly wired to Express-Session (which ours is), it will attach a user object to all requests for the duration of the session. We can manipulate that duration, but for now we’re using Passport’s default, which sets a browser cookie and allows the session to persist until the browser is closed or the server is reset, whichever comes first. So, if the user object exists, we have an active session, and can pass that information on to React.

Now we need to hit this endpoint. Save this file and let’s create some new actions. Open /src/actions/authentication.js and add the following lines to the bottom of the file:

export const sessionCheckFailure = () => ({ type: 'AUTHENTICATION_SESSION_CHECK_FAILURE' });
export const sessionCheckSuccess = json => ({ type: 'AUTHENTICATION_SESSION_CHECK_SUCCESS', json });

That’s easy enough, right? One action to send if our API returns no active session, and one action to send if it returns a user. Save this file, and then move on to /src/reducers/authentication.js. We need to add catches for our two new actions. First, DELETE lines 17 to 27. We’re going to shorten this file quite a bit, by piggybacking on existing code. Now, under line 16, add the following:

    case 'AUTHENTICATION_LOGIN_FAILURE':
    case 'AUTHENTICATION_SESSION_CHECK_FAILURE': {
      const newState = Object.assign({}, initialState);
      return newState;
    }

Here we’re taking advantage of how JavaScript switch statements let you use the same code for two or more different cases. In this case, we want to do the same thing whether the session check fails or a login attempt fails: replace our user state with blank values. We do that by borrowing the values defined in initialState above, but using Object.assign() to create a brand new object to return. You should always return brand new objects in reducers, to help avoid corruption in your state data.

Similar to how we’re doubling up on failures, we can double up on successes. To catch our successful session check, all we need to do is change line 22 from:

    case 'AUTHENTICATION_LOGIN_SUCCESS':  {

to:

    case 'AUTHENTICATION_LOGIN_SUCCESS':
    case 'AUTHENTICATION_SESSION_CHECK_SUCCESS': {

We’re good here. Save the file and open up /src/components/TemplateContainer.jsx. We’ve got a lot of work to do here, including converting this component from a pure function to a class. First we need a couple of imports, so under line 1, add the following:

import { bindActionCreators } from 'redux';

And under line 4, add this code:

import { sessionCheckFailure, sessionCheckSuccess } from '../actions/authentication';

Now we need to connect those two new actions, so we need a mapDispatchToProps function. Let’s add a padding line below line 12, and then put it on line 14. Here’s the code:

function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    sessionCheckFailureAction: sessionCheckFailure,
    sessionCheckSuccessAction: sessionCheckSuccess,
  }, dispatch);
}

And of course we need to add mapDispatchToProps to our connect function on line 27, like this:

export default connect(mapStateToProps, mapDispatchToProps)(TemplateContainer);

Good, now we can use those action creators. Let’s jump into converting our main function to a class. Start by changing line 7 from this:

function TemplateContainer(props) {

to this:

class TemplateContainer extends React.Component {

We’ll need a constructor, so under line 7, add the following code:

  constructor(props) {
    super(props);
  }

ESLint will complain that this constructor is useless. Don’t worry, that won’t last. Also, our return block is now broken, because it needs to be wrapped in a render() method. Change lines 12-15 to look like this:

  render() {
    const { authentication, progress } = this.props;
    return (
      <Template progress={progress} authentication={authentication} />
    );
  }

It’s time to write our method for checking sessions. This one’s big, but very similar to the async method we wrote for our log-in page container. Put it below the constructor, but above the render block. Here it is:

  async checkSession() {
    const { sessionCheckFailureAction, sessionCheckSuccessAction } = this.props;
    // contact the API
    await fetch(
      // where to contact
      '/api/authentication/checksession',
      // what to send
      {
        method: 'GET',
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (json.username) {
        sessionCheckSuccessAction(json);
      } else {
        sessionCheckFailureAction();
      }
    })
    .catch((error) => {
      sessionCheckFailureAction(error);
    });
  }

In brief, what this is doing is contacting our API endpoint to see if a session exists and firing off the appropriate actions depending on whether the API sends a complete user object, or an empty object.

That credentials: 'same-origin' line is important. Without it, fetch doesn’t send along the session cookie stored in the browser, which will make Express think the request is coming from a new session every time, so you’ll always get a blank object returned from your API.

Now we need to do two things. The first is bind this function in our constructor, so add a spacing line below line 9, and then add this code:

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

The second thing is that we need to actually fire checkSession! We’re going to do this using something called a component lifecycle method. These are specially-named functions that React will automatically run at certain times as a component is mounted (that is: made active) and unmounted. For example, our Log In page component is only mounted when we’re actively viewing the log in page. When we’re viewing, say, the home page, the Log In page component is unmounted; React essentially forgets it exists until we navigate to it again. This helps with memory management, among other things.

There are several component lifecycle methods, and we’re going to cover them in more depth moving forward. There’s a set that’s called when a component is mounting, a set that’s called when a component is updating (for example, if its props change), and a set that’s called when a component is unmounting. These are always called in a rigid order. Let’s take a look at mounting, since that’s what we need to work with. The order of the methods is:

constructor()
componentWillMount()
render()
componentDidMount()

This means if you put code in componentWillMount() it is guaranteed to run before the component renders, and if you put code in componentDidMount() it is guaranteed to run afterwards. That’s super useful, and you’ll find yourself using component lifecycle methods frequently as you build React apps. Today we only need one – we want to check for a session before we mount our Template Container. It’s general practice to put lifecycle methods above custom methods, so, above the checkSession method, add the following:

  componentWillMount() {
    // Before the component mounts, check for an existing user session
    this.checkSession();
  }

That’s it. We’ve got everything we need to make our session check work. Here’s the entire file:

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Template from './Template';
import { sessionCheckFailure, sessionCheckSuccess } from '../actions/authentication';

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

    // bound functions
    this.checkSession = this.checkSession.bind(this);
  }

  componentWillMount() {
    // Before the component mounts, check for an existing user session
    this.checkSession();
  }

  async checkSession() {
    const { sessionCheckFailureAction, sessionCheckSuccessAction } = this.props;
    // contact the API
    await fetch(
      // where to contact
      '/api/authentication/checksession',
      // what to send
      {
        method: 'GET',
        credentials: 'same-origin',
      },
    )
    .then((response) => {
      if (response.status === 200) {
        return response.json();
      }
      return null;
    })
    .then((json) => {
      if (json.username) {
        sessionCheckSuccessAction(json);
      } else {
        sessionCheckFailureAction();
      }
    })
    .catch((error) => {
      sessionCheckFailureAction(error);
    });
  }

  render() {
    const { authentication, progress } = this.props;
    return (
      <Template progress={progress} authentication={authentication} />
    );
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators({
    sessionCheckFailureAction: sessionCheckFailure,
    sessionCheckSuccessAction: sessionCheckSuccess,
  }, dispatch);
}

function mapStateToProps(state) {
  return {
    progress: state.progress,
    authentication: state.authentication,
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(TemplateContainer);

Save this file, then flip to a terminal window quickly and manually reset your server (if you’re running nodemon, just type rs and hit enter). This will make sure we’ve cleared any sessions. Now head to your browser, and hard-refresh your page. No user session, and a log in link. That’s what we want.

Now log in with an authenticated user. This will work exactly the same as it did in the previous tutorial. Once logged in, all you have to do is hard-refresh your page (and hide your Redux developer tools with ctrl-h). Presto! You’ve got a logged in session even after a refresh. If you check your browser console, or show your Redux devtools, you’ll see we’ve sent a session check success action.

Now that we can’t use a refresh to clear our user state, we’ll need a logout link and associated actions / reducers. We’ll build those in the next tutorial. See you then!

« Previous Tutorial Next Tutorial »