More Robust Request Tracking


In the previous video we looked at how we might display a little spinner icon on our 'Log In' button whenever a user tried to log in. They fill in their details, click submit, and whilst the web request takes place, they should see a spinning icon to show 'something' is happening. It's the classic modern web thing to do.

However, this approach falls down quite quickly if the form submission is a little more involved. I know this as I experienced this first hand whilst trying to implement a Registration workflow. Let's cover off what I did, and what went wrong.

From the end user's perspective, the form looked simple enough. There was the usual user name, email, repeated password. Also being a registration from I had a drop down for a payment plan, and an area to enter card details.

Behind the scenes, however, this wasn't as straightforward.

In case you've never had to build a system like this before, let me tell you one big part of the process is around handling customer's credit card details. The key piece I never knew until I started doing this: don't store customer's credit card details. Ever.

Companies like Stripe, BrainTree, and a whole bunch of others exist to mitigate you from this problem. See, protecting credit card details is - as you likely expect - a total nightmare. There are laws covering this stuff, and it's not something you should undertake lightly. Once you start saving this info you become a target for all manner of nefarious do-badders.

Anyway, instead you defer this pain to your billing provider - e.g. Stripe - and they charge you in a variety of ways for them assuming all this risk.

So, as interesting as this is, what you first need to do is to send off the customer's credit card details to e.g. Stripe, who then validate and verify, and if all goes to plan, they send you back a token (think: an id) representing the customer card details. But you're not done with Stripe just yet.

However, now that you have this card token you can next go ahead and create the user on your system.

Assuming this piece goes to plan, you can then use the token to create a new customer at your billing provider (e.g. Stripe), and then save the billing providers customer's id as a property of the new user object.

I know, right, what a nightmare.

There are so many potential ways this can go wrong.

But let's assume it works. The happy path. And let's get back to our basic form implementation.

Let's mock up a basic starting point for Registration - don't blindly copy this, it's terrible:

export function *doRegistration(action) {

  try {

    yield put(startSubmit, 'registration');

    const {
      username,
      email,
      password,
      password_repeated,
      chosen_payment_plan
      credit_card_number,
      credit_card_cvc,
      credit_card_month,
      credit_card_year,
    } = action.payload;

    const card_token = yield call(
      stripe.cardToken,
      credit_card_number,
      credit_card_cvc,
      credit_card_month,
      credit_card_year
    );

    const responseBody = yield call(api.registration, username, email, password, password_repeated, card_token);

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

  } catch (e) {

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

  } finally {

    yield put(stopSubmit, 'registration');

  }
}

Ok, there are many things wrong here. We're doing waaay too much.

The errors that Stripe returns won't be like the errors our Symfony API returns. Trying to meld both of them into our catch block is going to be painful.

About the only thing going for this monstrosity is that props.submitting should work as expected :) Every cloud.

So, we do the obvious and split out 'card token' creation into one saga, and Symfony API user registration into another saga, and then we use a third saga to watch for the initial form submission.

This is better, but the startSubmit and stopSubmit calls now no longer live within the same function. Heck, they likely don't even live in the same file. And not only is that confusing, it's also tying the sign up process to your form.

A Master Request

We don't want the startSubmit / stopSubmit functions confusingly added to some of our saga functions. Well, I didn't. You might, and that's fine.

My next thought then was to have a Reducer which tracked the status of requests.

I could then do something like this:

export function *doLogin(action) {

  try {

    yield put({
      type: types.SENDING_REQUEST,
      payload: {
        sendingRequest: true
      }
    });

    // * snip *

  } catch {

    // * snip *

  } finally {

    yield put({
      type: types.SENDING_REQUEST,
      payload: {
        sendingRequest: false
      }
    });

  }

And with a simple requestReducer, I could easily track if there was a request in progress without being directly tied to Redux Form:

// /src/reducers/requestReducer.js

import {SENDING_REQUEST} from '../constants/actionTypes';

export default function request(state = {
  sendingRequest: false
}, action) {

  switch (action.type) {

    case SENDING_REQUEST: {
      return Object.assign({}, state, {
        sendingRequest: action.payload.sendingRequest
      });
    }

    default: {
      return state;
    }
  }
}

And this works well. For doLogin, anyway.

Let's switch back to the Registration workflow though, and imagine we've added the put for SENDING_REQUEST to the Stripe card token call, and to our Symfony API registration call.

The user fills in the form and clicks the 'submit' button.

The spinner shows. The world feels like a happy place.

The user gave some good credit card information. Wonderful. Stripe kindly does its thing and returns us the card_token.

The spinner stops. (What?!)

The registration process continues. The spinner starts again. The customer gave some good data for their profile, and that goes through as well. The spinner stops again... huh?

What went wrong here?

Well, technically nothing went wrong. It behaved as we told it too. First the Stripe request started, and dispatched the action to update the sendingRequest to `true.

This action was handled by our requestReducer, which in turn updated the application state. We then mapStateToProps for this portion of the state, to use the value of sendingRequest as the prop of isSubmitting, which our LoginForm uses to determine what Button text variant to show.

With sendingRequest: true, we show the spinner. The card_token is created and returned. This completes this part of the saga, which in turn (finally) does a put to say sendingRequest: false.

By way of the Reducer, our state is updated appropriately. This stops the spinner, and resets the 'Registration' button back to the 'inactive' variant.

Then the next step in the saga is triggered, which in turn put's a sendingRequest: true, which then causes the button to switch back to 'Registering...' and to show the spinner. All the while the end user is left confused, watching this button flick between looking like it's working, and it's stopped working. Not good.

A Better Implementation

There's likely a bunch of ways we could solve this problem.

My way is to give a name to each of the requests, and then add and remove individual requests from a single array kept on the requestReducer.

This is more complicated, and whilst we could do this without tests, it would more than likely lead to a source of bugs.

Adding tests is important not just for this step, but in general. So we're going to use this in the very next video as an excuse to get testing hooked up.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes