Login - Part 3

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are continuining on with the Login journey. By the end of the previous video we had learned how to use the dispatch function on the Redux store to send out an Action (a Flux Standard Action) that described the activity that had just taken place.

We discussed how the call to login may result in numerous outcomes. We could be successful, or we may be unsuccessful because the username or password were incorrect, or perhaps the connectivity to the back end API itself had been interrupted.

As a result of this, we cannot directly send this action to a Reducer.

Instead, we must handle the function that involves side effect - a call to our back end API - and then once we have the response, be it success or failure, only then can we actually send this resolved data to our reducer.

To start with we must implement some functionality that can 'watch' for this initial action taking place. This is where Redux Saga comes in to play.

To recap, we have already done the groundwork to get Redux Saga connected to the Redux Store.

Creating Our Auth Saga

This means our next step can be to start the creation of our first saga. In our case, this will be auth.saga.js, though the naming convention is not essential to follow:

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

export function *doLogin(action) {

}

We've created the file, and exported a generator function.

You can tell it's a generator function as it's preceeded by a *.

This may be your first time encountering a generator function. And if you're anything like me, this can be a cause for concern - another new concept to learn? Good grief.

Thankfully, as you will see, using generator functions in Redux Saga is really nothing to worry about. You can get started without really understanding how they work, though a truly fantastic way to learn about them is by using Redux Saga :) Another really great place to start learning is from the brilliant book Understanding ES6 by Nicholas C. Zakas. And another would be ES6 Katas - one a day keeps unemployment away.

A generator function is very similar to a regular JavaScript function, only when invoked a generator function returns an iterator.

I think that's partly why wrapping your head around generators is hard: to learn one new concept you must first learn another.

An iterator is not a concept unique to JavaScript. PHP has iterators, as do many other languages.

An iterator is an object that allows traversal over itself. This iterator has a next() function, which as you might expect, returns the next element in its collection. It will also have a done() function, which returns true if there are no more elements in the collection, or false if elements remain.

Creating this iterator by hand is possible, but cumbersome. Thankfully, as mentioned, invoking a generator function will return you an iterator.

Another key part of generator functions is that you can pause their execution by using yield.

You can then resume the generator function by calling next().

The most mindbending part of generators - for me at least - was in discovering that you can pass in variables after each yield. Whilst initially very confusing, this actually makes testing our Redux Sagas extremely easy.

Now, I truthfully don't expect you to be that much the wiser on generator functions at this stage. I would advise that you play around with these code samples, then continue on with the course and see how they work in practice. Once you have seen how they work, and played around with them yourself, I would be willing to bet they suddenly start to make a lot more sense.

Basic Auth Saga Implementation

We're going to start with the most basic approach - the happy path - and build up from here.

We know that the action that's given to our *doLogin generator function is going to have the shape:

{
  type: types.LOGIN__REQUESTED,
  payload: {
    username,
    password
  }
}

Remember, this is ES6, so an object key / value pair can be shortened to just the key if the value is a variable of the same name.

In other words:

{
  type: types.LOGIN__REQUESTED,
  payload: {
    username: username,
    password: password
  }
}

Is identical to the first example, but ES6 allows us to skip the redundancy.

So, we know we have this action with the payload of username / password. We can use another ES6 concept - object destructuring - to pull out these variables from our action.payload:

export function *doLogin(action) {

  const {username, password} = action.payload;

}

Awesome. Now we have two variables - username, and password that contain the values from our action's payload.

Next, we want to use these values to make a login request to our API. Remember, the API already exists, it's the Symfony 3 powered API from this project. However, this concept applies to literally any API you would be talking too, it's completely back end agnostic.

API Communication

We don't yet have a function defined to interact with our API, so let's define one:

// /src/connectivity/api.js

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 = response.json();

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

  // throw - to be implemented
}

Again, new things happening here.

Firstly, this function is async. This allows us to then use await inside the function, which is similar to the way generators work in that we can let JavaScript worry about 'pausing' whilst something happens. In our case, we want to pause execution of the login function whilst the outcome of calling our API's /login endpoint takes place.

The nice thing about this is that we no longer have to (directly) use Promises. This is good as our code immediately becomes more readable.

However, under the hood, async functions are using both Promises and generators, so to fully grasp how async can be used, a thorough understanding of both concepts is still required.

Secondly, in reality we should be wrapping this function in a try / catch block. There's a chance our call may fail - the API may be down, or similar - and we don't want to rely on a downstream blow up returning a useful error. We will fix this shortly, but for now... happy path.

Thirdly, we are making use of fetch. This is really nice replacement for XMLHttpRequest, which was a bit of a beast.

Typically you see fetch('someUrl', objFullOfOptions).then(...), but we don't need to do the then bit, as we're using await. Nice. We gain all the benefits of Promises with - as best I can tell - none of the downsides.

Lastly, we can't just return the outcome of our fetch request. We must convert the contents of the response body (a ReadableStream) into JSON.

Of course the URL and request config are largely hardcoded here, and we will likely want to make many further calls to different API endpoints. We will refactor this in due course. For the moment, we will start off hardcoded, and refactor later as appropriate.

Calling The API

We've got the startings of our Auth Saga, and now we have some way of establishing connectivity with our API.

The next thing we must do is call the login function that really talks to our API.

Remember how Redux Saga is all about handling side effects?

Well, the first 'effect' we want to create is this call to api.login:

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

import * as api from '../connectivity/api';
import {call} from 'redux-saga/effects';

export function *doLogin(action) {

  const {username, password} = action.payload;

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

}

Again, lots happening here.

Firstly, we've imported the api functions from our api.js file. Straightforward enough. There may be many functions in this file shortly (currently we have only one, the login function), so rather than worry about importing them all individually, we import the whole lot at once and put them under the api alias.

Secondly, remember that our *doLogin function is a generator function.

We know that with generator functions we can yield a value, which will pause our function's execution.

Two questions I had - what are we yield'ing too, and why don't we need to call next()?

Well, if you remember, during our development environment setup we added the sagaMiddleware to our stack of Redux middlewares. Then, we created a new file - src/sagas/index.js - which export a single, default function called *rootSaga():

// /src/sagas/index.js

import {fork} from 'redux-saga/effects';

export default function *rootSaga() {
  yield [

  ];
}

We hooked up this function inside configureStore.js via sagaMiddleware.run(sagas);.

To come back to our questions then:

what are we yield'ing too?

The Redux Saga middleware.

The middleware suspends the saga until the outcome of our yielded side effect completes.

And why don't we need to call next()?

Because as soon as we get a resolution to the outcome of this yielded side effect, the Redux Saga middleware calls next() for us, and on we go until the next yield, or the outcome of done() is true.

It's also important to understand that when we yield the Redux Saga effect of call, this is really a only a description of what we want to happen, not the actual outcome of what happens. Ultimately our requested function call will take place, but from the perspective of our *doLogin function, all we did was yield an object detailing the instructions of what we want to actually happen.

And this is awesome because testing becomes an absolute breeze. Rather than having to start mocking API calls, instead we simply assert that the yielded descriptive object matches the one we provide. Again, I don't really expect this to be very clear at this point, but when you see it in a few videos time, I hope you have the same reaction I did (mind: blown!).

Who Watches The Watchmen?

But we have a couple of problems still to address.

Firstly, how does this *doLogin function 'hear' about our action?

And secondly, how do we hook this up to our *rootSaga?

Well, fortunately answering the first question will help answer the second.

Remember that Redux Saga is hooked up to the Redux Store.

As such, it can listen for - or in the parlance of Redux Saga - watch for actions.

We can therefore define ourselves another generator function called *watchLogin, which will describe what should happen when a certain action is seen:

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

import * as api from '../connectivity/api';
import {call} from 'redux-saga/effects';
import {takeLatest} from 'redux-saga';
import * as types from '../constants/actionTypes';

export function *doLogin(action) {

  const {username, password} = action.payload;

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

}


export function *watchLogin() {
  yield takeLatest(types.LOGIN__REQUESTED, doLogin);
}

In essence, when we 'see' an action with the type of types.LOGIN__REQUESTED, then actually doLogin.

The use of the takeLatest Saga Helper function is to ensure that we only ever act on the most recent attempt to log in.

There is an alternative - takeEvery - which would call the doLogin function again and again, even if a log in was already in process, should the user hammer the login process somehow, for some reason. In the case of login, only allowing the very latest attempt seems prudent. Other cases vary, so knowing about both is helpful.

We've got our *watchLogin function all set up. We're sorted on the first step: how does this *doLogin function 'hear' about our action.

Now we need to connect this *watchLogin function to our *rootSaga:

// /src/sagas/index.js

import {fork} from 'redux-saga/effects';
import * as authSaga from './auth.saga';

export default function *rootSaga() {
  yield [
    fork(authSaga.watchLogin)
  ];
}

fork is another Redux Saga effect. Crucially by using fork we can ensure that authSaga.watchLogin doesn't block the thread. Our *rootSaga won't be blocked by authSaga.watchLogin, nor any other forked functions we add to this array.

Login Success

At this stage, assuming your API is set up properly and you are sending in some valid credentials, you should be receiving a valid JSON Web Token (JWT) as part of the response body.

Now, we don't want to concern ourselves with actually handling much more than we already do. Our *doLogin function does exactly what it says. It does the login, but it doesn't worry about whether the login succeeded, or failed.

Instead, we should dispatch another action describing the outcome:

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

import * as api from '../connectivity/api';
import {call, put} from 'redux-saga/effects';
import * as types from '../constants/actionTypes';

export function *doLogin(action) {

  const {username, password} = action.payload;

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

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

// omitted for brevity

To finish up here we describe what just happened. We are only concerning ourselves with the happy path. We will start to address failures in the next few videos. But essentially this pattern is to be repeated whether things went right, or wrong.

We yield again, this time describing the successful outcome. We use put, which is an effect instructing the Redux Saga middleware to dispatch an action to the Redux Store. Pretty useful :)

The next 'hop' may be another function inside our saga, or it may be a Redux reducer. In our case, it will be another saga function.

For the moment we will define the 'shell' of these two possible outcomes, and only concern ourselves with the actual implementations in the next video:

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

import * as api from '../connectivity/api';
import {call, put} from 'redux-saga/effects';
import {takeLatest} from 'redux-saga';
import * as types from '../constants/actionTypes';

export function *doLogin(action) {

  const {username, password} = action.payload;

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

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

export function *watchLogin() {
  yield takeLatest(types.LOGIN__REQUESTED, doLogin);
}



export function *doLoginSucceeded(action) {

}

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



export function *doLoginFailed(action) {

}

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

And of course, we need to update the *rootSaga:

// /src/sagas/index.js

import {fork} from 'redux-saga/effects';
import * as authSaga from './auth.saga';

export default function *rootSaga() {
  yield [
    fork(authSaga.watchLogin),
    fork(authSaga.watchLoginSucceeded),
    fork(authSaga.watchLoginFailed),
  ];
}

Interestingly a big part of Sagas are in how they allow us to handle failure. Within this pattern we actually address failure up front, which really helps create a robust front end system.

And that's exactly what we will get to in the next video.


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