Logout

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are going to implement the Logout flow. This is very similar to our Login flow in that we will still need to dispatch Actions, use Sagas, and the Reducer, but the implementation will of course, be different.

From a very high level the idea here is as follows:

  1. We have a /logout route
  2. When that route is visited, an Action is dispatched to start the Logout process
  3. A Saga watches for this Logout action, and does two things: a. alerts the reducer (by way of dispatching a new Action) b. Redirects the User to the / (site root)

There's no requirement to talk to the API at all during the logout process.

When we log in, we get given a JWT. Currently we are not storing this JWT at all - we will address this in a future video. For now, just be aware that it is the client's responsibility to keep this JWT 'around' somewhere / somehow.

To log out, all we need to do is delete / forget about the JWT.

If we wished to get a little more creative - maybe giving our server-side some facility to invalidate active JWTs - we would need to add additional logic. This isn't something we have currently, but would be done on the API-side (in Symfony, in our case).

Technically then, as we aren't keeping the JWT around yet, we are logged out immediately after logging in! :)

However, we have jumped ahead a little bit during our login flow to act upon the successful log in. We are decoding the JWT, extracting the Username and User ID from the JWT itself, and then we save this information - along with a little helper boolean value under isAuthenticated. All this comes under our authReducer.

Then, in the previous video we learned how we could save / persist this chunk of state to some local storage - in our case localStorage.

We could then browse around our site, checking the state to see if we should consider ourselves to be 'logged in' or not.

To tidy up on 'log out', we now want to erase this locally stored state.

Then, once we have removed this state, we want to redirect our user to the / route, to ensure they aren't left on some route they no longer have access too.

Again, this is all largely silly at this stage as we have so little happening in our application. But let's not worry about that right now.

Creating The Log Out Page

It might seem a little unintuitive to need a 'page' to handle log out. After all, the user will never actually see anything on this page - you visit the /logout URI and you should - hopefully - be logged out and redirected back to the home page.

However, by making the Log Out Page its own Page (well, a Container in our case), we can hook into React Component Lifecycle methods. This allows us to call our own functions when the page is requested.

This is best illustrated by way of our requirements:

We have a /logout route

We create a new React component called the LogoutPage, and hook this up to React Router under the /logout route:

// /src/routes.js

import React from 'react';
import { Route, IndexRoute } from 'react-router';

import App from './components/App';
import HomePage from './components/HomePage';
import LoginPage from './containers/LoginPage';
import LogoutPage from './containers/LogoutPage';
import AboutPage from './components/AboutPage.js';
import NotFoundPage from './components/NotFoundPage.js';

export default (
  <Route path="/" component={App}>
    <IndexRoute component={HomePage}/>
    <Route path="login" component={LoginPage}/>
    <Route path="logout" component={LogoutPage}/>
    <Route path="about" component={AboutPage}/>
    <Route path="*" component={NotFoundPage}/>
  </Route>
);

When that route is visited, an Action is dispatched to start the Logout process

Great, now we have this page that can be visited by heading over to /logout.

The shell of this page is a basic React class:

// /src/containers/LogoutPage.js

import React, {Component} from 'react';

class LogoutPage extends Component {

  render() {
    return null;
  }
}

export default connect()(LogoutPage);

If you visit this page it doesn't have any content, but as our Route definition for this route nests the page under the App component, then we still see the top bar and anything else on our top level component.

By adding in a React Component Lifecycle method of componentWillMount, we can define some logic to run whenever this page is requested:

// /src/containers/LogoutPage.js

import React, {Component} from 'react';
import {connect} from 'react-redux';
import * as types from '../constants/actionTypes';

class LogoutPage extends Component {

  componentWillMount() {
    this.props.dispatch({
      type: types.LOGOUT__REQUESTED
    })
  }

  render() {
    return null;
  }
}

export default connect()(LogoutPage);

There's two interesting things happening here:

Firstly, we are using componentWillMount. In other words, whenever this page is requested, dispatch an Action with type of LOGOUT__REQUESTED. We'll cover that more in a moment.

Secondly, and as we have already covered, we need to connect our Page before we gain access to dispatch.

A Saga watches for this Logout action...

We have already seen the fundamentals of how our Sagas will watch for specific Action types, and this is no different:

// /src/sagas/auth.saga.js
// * snip *

export function *doLogoutRequested() {

  // now we need to:
  // a. alert the reducer (by way of dispatching a new Action)
  // b. Redirect the User to the `/` (site root)

}

export function *watchLogoutRequested() {
  yield takeLatest(types.LOGOUT__REQUESTED, doLogoutRequested);
}

Let's cover the piece we already know how to do - alerting the reducer by dispatching another action:

// /src/sagas/auth.saga.js
// * snip *

export function *doLogoutRequested() {

  // now we need to:
  // a. alert the reducer (by way of dispatching a new Action)

  yield put({
    type: types.LOGOUT__COMPLETED
  });


  // b. Redirect the User to the `/` (site root)

}

export function *watchLogoutRequested() {
  yield takeLatest(types.LOGOUT__REQUESTED, doLogoutRequested);
}

Simple enough - but do make sure to define these new constants.

Then we can add in a case to our authReducer's switch statement:

// /src/reducers/authReducer.js

import * as types from '../constants/actionTypes';

export default function auth(state = {
  isAuthenticated: false,
  id: undefined,
  username: undefined
}, action) {

  switch (action.type) {

    case types.LOGIN__COMPLETED: {
      const { id, username } = action.payload;
      return Object.assign({}, state, {
        isAuthenticated: true,
        id,
        username
      })
    }

    case types.LOGOUT__COMPLETED: {
      return Object.assign({}, state, {
        isAuthenticated: false,
        id: undefined,
        username: undefined
      })
    }

    default: {
      return state;
    }
  }
}

Again, we have covered the foundations of this during our Login flow, so there shouldn't be any new concepts to introduce at this stage. Essentially we just wipe out anything set in our auth state.

The more tricky part is initiating the redirect of our user back to /:

// /src/sagas/auth.saga.js
// * snip *
import {push} from 'react-router-redux';

export function *doLogoutRequested() {

  // now we need to:
  // a. alert the reducer (by way of dispatching a new Action)

  yield put({
    type: types.LOGOUT__COMPLETED
  });


  // b. Redirect the User to the `/` (site root)
  yield put(
    push('/')
  );

  // or - perhaps more technically correct
  yield call(push, '/')
  );
}

export function *watchLogoutRequested() {
  yield takeLatest(types.LOGOUT__REQUESTED, doLogoutRequested);
}

This will allow you to 'push' a new location on to the history, making this (/, or whatever route you desire) the app's currently active location.

But this won't immediately work.

To make this work we must hook up the React Router Redux middleware:

import {routerMiddleware} from 'react-router-redux';
import {browserHistory} from 'react-router';

const routerMw = routerMiddleware(browserHistory);

function configureStoreDev() {
  const middlewares = [
    routerMw,
    sagaMiddleware,
    loggerMiddleware
  ];

  // * snip *

Of course there's a whole bunch more things happening in the configureStore file, and I have just included the bits here that make sense in the context of what we are doing, but do be sure to check out the full file for the real contents. Also, make sure to do this for both dev and prod.

As you will see in the video, we must also do this for our Login Page journey, otherwise when logging in the User would not be redirected as would make most sense from a usability perspective. Instead, they would be left on /login, even though they would now - potentially - be logged in.


Code For This Course

Get the code for this course.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 App Walkthrough - User Experience 03:15
2 App Walkthrough - Developer Experience 07:41
3 Development Environment Setup 06:34
4 Login - Part 1 09:15
5 Login - Part 2 07:55
6 Login - Part 3 12:37
7 Login - Part 4 10:22
8 Login - Part 5 08:00
9 Saving Redux State to Local Storage 08:50
10 Logout 10:57
11 Adding an Auth-aware NavBar 14:43
12 Cleanup, Linting, and Login Form Styling 09:58
13 Showing Spinning Icons, Because Why Not? 08:11
14 More Robust Request Tracking 09:07
15 Getting Started Testing With Jest 06:43
16 Testing Request Reducer - Part 1 11:35
17 Testing Request Reducer - Part 2 05:25
18 Testing AuthSaga - Happy Path 09:19
19 Testing AuthSaga - Unhappy Paths 04:38
21 Testing JavaScript's Fetch with Jest - Happy Path 05:15
21 Testing JavaScript's Fetch with Jest - Unhappy Paths 04:35
22 Getting Started with Jest Mocks 08:52
23 Using Webpack Environment Variables in Jest Tests 09:37
24 User Profile Page - Part 1 07:31
25 User Profile Page - Part 2 10:25
26 User Profile Page - Part 3 07:23
27 Change Password - Part 1 10:01
28 Change Password - Part 2 07:59
29 Change Password - Part 3 - Displaying Errors 06:28
30 Change Password - Part 4 - Converting Errors From Symfony to Redux Form 05:39
31 Change Password - Part 5 - Adding More Tests 05:06
32 Change Password - Part 6 - Avoid Blocking, and Wrap Up 06:23
33 Registration - Part 1 08:50
34 Registration - Part 2 06:25
35 Registration - Part 3 05:25