Showing Spinning Icons, Because Why Not?


Up until now we've essentially brought the back-end mentality to our front-end. We've worked with a whole bunch of computer sciency concepts such as generator functions and reducers, and done a lot of the ground work required to make a modern front-end project actually work.

Now it's time to have a bit more fun.

Well, ok, this won't be the kind of fun that you experience playing the latest Battlefield, or FIFA (or whatever else you do that's that sort of fun). Instead, in this video we will do a task that our end users will see. In my discussions with devs over the years, this is the kind of thing that back-end developers think front end developers do all day :)

Yes, we are going to add a spinner.

We'll also get React to show two variations of our button text:

  • 'log in' when the button hasn't been pressed
  • 'logging in...' and a spinner only when the log in process takes place

There's loads of ways you could do this. What I am about to show you is simply one way.

But the really nice thing is: we've already done all the hard work.

As it happens, our LoginForm component is blissfully unaware as to 'how' the login process takes place. All it knows is that it is a form which:

  1. calls a provided function when the 'Log in' button is clicked, and;
  2. is told (via props) whether it is in the state of submitting or not.

This is fairly awesome as it means we can test our component in isolation. It 'reacts' (ahem) only to the current state of its props.

All we need to do is set up our LoginForm component in such a way that the value it receives for its submitting prop is directly hooked up to our application's state (helped along via Redux) and we are good to go. There's very little complication to this, so let's just see it in action:

// /src/components/LoginForm.js

import React from 'react';
import {Field, reduxForm} from 'redux-form';
import {Button} from 'reactstrap';

const LoginForm = (props) => {
  return (
    <form onSubmit={props.handleSubmit} className="form-signin">

      <Field component="input"
             name="username"
             type="text"
             placeholder="Username or email address"
             required="required"
             className="form-control"
      />

      <Field component="input"
             name="password"
             type="password"
             placeholder="Password"
             required="required"
             className="form-control"
      />

      <Button type="submit"
              size="lg"
              block
              color="success"
      >
        {props.submitting ?
          <span>
            <i className="fa fa-spin fa-spinner"/> Logging In...
          </span>

          :

          <span>Login</span>
        }
      </Button>
    </form>
  );

};

LoginForm.propTypes = {
  handleSubmit: React.PropTypes.func.isRequired,
  onSubmit: React.PropTypes.func.isRequired,
  submitting: React.PropTypes.bool.isRequired
};

export default reduxForm({
  form: 'login'
})(LoginForm);

The keen eyed may have noticed we specify the onSubmit propType, but we don't directly use it. We've talked about this in previous videos.

Adding Font Awesome

We are using the excellent Font Awesome for our icons. Animating the spinner is as simple as putting the CSS class (className in React-land) of fa-spin and wayhey, we have ourselves a spinning icon.

To make font awesome work in our project, we must first do yarn add font-awesome, and then importantly, actually pull the stylesheet into our project:

// /src/components/App.js

// other import statements

import font from 'font-awesome/css/font-awesome.css'; // eslint-disable-line

And we should be good to go.

Hardcoded Props

Anyway, as mentioned, our component receives some props, one of which will be submitting. This value must be a bool, but how that value is determined is not really any concern of the LoginForm.

Knowing that we have this bool, we can write a ternary:

// /src/components/LoginForm.js

{props.submitting ?
  <span>
    <i className="fa fa-spin fa-spinner"/> Logging In...
  </span>

  :

  <span>Login</span>
}

And awesome, we've nailed part of our goal for this video. That is, if we set the prop.submitting to true, then we see the spinner and the expected text, otherwise if false, we just show the plain 'Log In' text.

We can try this right now by hardcoding the prop:

// /src/containers/LoginPage.js

  render() {
    return (
      <LoginForm
        onSubmit={this.doLogin.bind(this)}
        submitting={true}
      />
    );
  }

The clever part about this is not what your end user sees. Showing a spinner and a variation of the button text is not complicated. What is complicated - if not done in a structured manner - is determining what the current value of props.submitting should be.

Now, there's a bunch of ways to figure out whether our form is currently being submitted. Redux Form - which we are using - provides a prop of submitting which we could use. We could also manage this ourselves. Both are valid approaches.

To begin with, we will use the Redux Form approach. This involves calling their startSubmit and stopSubmit functions whenever we want to start and stop submitting a form.

Later, we will implement our own logic for managing the current state of whether a request is in progress or not. The reason for this is that as our application grows in complexity, a single form submit may involve multiple web requests - some to our server, some to other servers - but all within the apparently lifecycle of a single form submit (from the perspective of our end users, anyway). An example of this might be in Registration, whereby first we need to send the customers card details to Stripe, and then send that response on as part of the Registration data to our own API.

Can You Start The Fans, Please!

If we don't explicitly tell Redux Form that we are starting and stopping submitting our form, then it will not correctly update its props. This means we can't use props.submitting.

Thankfully, telling Redux Form about our form's submission status is easy. We just need to dispatch the startSubmit and stopSubmitfunctions inside our*doLogingenerator function insideauth.saga.js`:

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

import * as api from '../connectivity/api';
import {startSubmit, stopSubmit} from 'redux-form';
import {call, put} from 'redux-saga/effects';
import {takeLatest, delay} from 'redux-saga';
import jwtDecode from 'jwt-decode';
import {push} from 'react-router-redux';
import * as types from '../constants/actionTypes';

export function *doLogin(action) {

  try {

    yield put(startSubmit, 'login'); // << this line

    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
      }
    });

  } finally {

    yield put(stopSubmit, 'login'); // << and this line

  }
}

As we are wrapping our function body in a try / catch block we can make use of finally to ensure that the form submission state is always left in the correct state (stopSubmit) regardless of whether an error is thrown, or the function runs as expected. Further reading via MDN if interested.

There's some stuff in here that I'm not happy about. Notice that we throw an Error inside the try block, but what happens if we throw this - our catch block yield's a put which expects a stateCode... this is how bugs start to creep in. Don't worry, we will clean up this soon when we get to testing.

Anyway, this now works, and our LoginForm's props should be updating appropriately.

We can therefore remove the hardcoded submitting prop and let Redux Form pass the prop through.

And this is all good, and may well be good enough for you. The only downside is - as mentioned earlier - we don't really have fine grained control over when requests start, and when they stop, and this leads to problems whereby the spinner may stop displaying before all the requests have finished. I mean, the spinner display is a real minor issue in the scheme of things - but it's a symptom of a bigger problem underneath. One we can, and will start to address in the very next video.

Code For This Course

Get the code for this course.

Episodes