Login - Part 4

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are continuining with our Login journey, this time firstly ensuring that should a bad outcome occur from our Login request that we are handling this situation gracefully. And secondly we will learn how to decode the returned JWT / JSON Web Token in a "happy path" login, and how this information will be handled downstream.

The first thing we need to address is in our implementation of the connectivity/api file, under the login function. When we originally implemented this function, we were covering a whole bunch of other stuff at the same time. I'm the first to admit: there's a ton of stuff to cover in this course. I find the best way to keep momentum and motivation is to make as much progress towards the happy path as possible, and then revisit each step as and when it becomes problematic.

So here we are.

We need to sort out what happens when the login journey goes badly.

There's a ton of ways this could happen:

  • Client lost connection to the server
  • API being offline / unavailable
  • Bad credentials
  • Server returning something crazy

Rather than trying to figure out what they all mean, let's:

  • convert the response body to JSON;
  • try and grab a message from the response body, and use that, or;
  • use the default message for the response code if not;
  • and pass back to the client, along with the status code (200, 403, etc)

Some code:

import HttpApiCallError from '../errors/HttpApiCallError';

export async function login(username, password) {

  const url = 'http://api.rest-user-api.dev/app_acceptance.php/login';

  const requestConfig = {
    method: 'POST',
    mode: 'cors',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      username,
      password
    })
  };

  const response = await fetch(url, requestConfig);

  const data = await response.json();

  if (response.status === 200) {
    return data;
  }

  throw new HttpApiCallError(
    data.message || response.statusText,
    response.status
  );

}

Where the HttpApiCallError is a JavScript class, defined:

// /src/errors/HttpApiCallError.js

export default class HttpApiCallError extends Error {

  constructor(message, statusCode) {

    super(message);

    this.message = message;
    this.statusCode = statusCode;

    this.stack = (new Error(message)).stack;
    this.name = 'HttpApiCallError';

  }

}

JavaScript's default Error is fairly raw in terms of functionality. We're adding some useful niceties here, that's all.

Redfine this however you like, of course.

Good, so we now throw if the API call goes wrong.

Why this ultimately matters to us though is that we can now define what should happen in the event of the "unhappy path". Or an "unhappy path", depending on how you look at these things.

Our *doLogin() Saga step can therefore be updated:

// /src/sagas/auth.saga.js

export function *doLogin(action) {

  try {

    const {username, password} = action.payload;

    const responseBody = yield call(api.login, username, password);

    if (typeof responseBody.token === "undefined") {
      throw new Error('Unable to find JWT in response body');
    }

    yield put({
      type: types.LOGIN__SUCCEEDED,
      payload: {
        idToken: responseBody.token
      }
    });

  } catch (e) {

    yield put({
      type: types.LOGIN__FAILED,
      payload: {
        message: e.message,
        statusCode: e.statusCode
      }
    });

  }
}

What's most interesting to me here is that this code is not what you would get if you wrote a test first.

But let's not worry about that right now, as we will shortly re-write all of this in a test driven manner. And the "why bother?" of that will be covered in greater depth then.

But it works enough, and now we can dispatch an action based on the outcome of either the success path, or that of any kind of failure we care about.

The idea here is simply delegation. Always push the decision as far up the tree - towards the API consumer - as possible. By which I mean we have now handled this thoroughly enough to have allowed other interested parties to work with the outcome, rather than each having to be cemented into this catch block.

You can now register interested listeners to this process with e.g.:

// /src/sagas/auth.saga.js

export function *doLoginFailed(action) {
  // e.g. trigger a Toast notification
}

export function *watchLoginFailed() {
  yield takeLatest(types.LOGIN__FAILED, doLoginFailed);
}

We might even do so, a little later :)

Now back on the happy path we care about the response. It contained a token in the body.

From that token, our client can determine two important values. You can add as many as you like.

In our case we have the user's id, and username.

The username is a vanity thing. People like to see their name in lights. We likely always want to display it in some capacity, so we don't want to have to make a bunch of requests just to figure this out.

Also, the user's id because we need it for pretty much anything.

// /src/sagas/auth.saga.js

// ** snip **
import jwtDecode from 'jwt-decode';


export function *doLoginSucceeded(action) {

  const {idToken} = action.payload;

  const {id, username} = yield call(jwtDecode, idToken);

  yield put({
    type: types.LOGIN__COMPLETED,
    payload: {
      id,
      username
    }
  });
}

export function *watchLoginSucceeded() {
  yield takeLatest(types.LOGIN__SUCCEEDED, doLoginSucceeded);
}

Remember, your calls in Redux Saga are descriptions of what should happen, not the actuality of the outcome.

We want to call the jwtDecode function here with the token we received.

Of course, the outcome to this call could fail. We should have this function body in a try / catch. Again, we will cover this during testing.

Again though, assuming the happy path allows us to progress towards an MVP. If that sucks, we don't want to have wasted too much effort.

And so we dispatch another Action, sending of the user's id and username to whoever may be interested in it.

No more Sagas at this point. We are now ready for... the Reducer.


Code For This Course

Get the code for this course.

Code For This Episode

Get the code for this episode.

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