Change Password - Part 3 - Displaying Errors

This video is available to view for members only.

Click here to Join!

Already a member?

Login


In this video we are going to get started adding in the error handling for our Change Password form. There's quite a lot to cover here, including:

  • Changing the styling to indicate which field(s) have an error / invalid state
  • Add in any error text returned by the API to the correct field(s)
  • Show an error state on both the Password and Password Repeated fields if either is invalid

The first two are fairly obvious, but the third is exactly the sort of thing I would forget to budget time for when estimating. And you will very much likely want this, so forgetting about it sucks.

The way in which all of these processes will begin is by dispatching a new action to indicate that we somehow failed changing the password.

As it stands, we have the following code in the doChangePassword function:

export function *doChangePassword(action) {

  try {

    const {userId, currentPassword, newPassword, newPasswordRepeated} = action.payload;

    yield put({
      type: types.REQUEST__STARTED,
      payload: {
        requestFrom: REQUESTS.PROFILE__DOCHANGEPASSWORD__SAGA
      }
    });

    const responseBody = yield call(api.changePassword, userId, currentPassword, newPassword, newPasswordRepeated);

    yield put({
      type: types.CHANGE_PASSWORD__SUCCEEDED,
      payload: {
        message: responseBody
      }
    });

  } catch (e) {

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

  } finally {

    yield put({
      type: types.REQUEST__FINISHED,
      payload: {
        requestFrom: REQUESTS.PROFILE__DOCHANGEPASSWORD__SAGA
      }
    });

  }
}

export function *watchChangePassword() {
  yield* takeLatest(types.CHANGE_PASSWORD__REQUESTED, doChangePassword);
}

This covers the happy path.

If the password change fails for any reason, the only thing we do is dispatch a REQUEST__FAILED action, which is generic, and not very helpful in this particular instance.

We know - by way of our Behat tests - that if our password change goes wrong we can expect our Symfony 3 API to send back something useful as part of the response body. An example of this might be:

# Symfony 3 API project
# /src/AppBundle/Features/password_change.feature#L58

  Scenario: Cannot change password with bad current password
    When I am successfully logged in with username: "peter", and password: "testpass"
     And I send a "POST" request to "/password/1/change" with body:
      """
      {
        "current_password": "wrong",
        "plainPassword": {
          "first": "new password",
          "second": "new password"
        }
      }
      """
    Then the response code should be 400
     And the response should contain "This value should be your current password."

It would be nice if we could map this / these errors back on to the respective form field(s).

To begin with then, we need to capture the response body, which we aren't doing in the generic REQUEST__FAILED action.

  // * snip *

  } catch (e) {

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

    yield put({
      type: types.CHANGE_PASSWORD__FAILED,
      payload: {
        response: e.response
      }
    });

  } finally {

  // * snip *

Don't forget to create and export this new constant in actionTypes, if you haven't already done so:

// /src/constants/actionTypes.js

export const CHANGE_PASSWORD__REQUESTED = 'CHANGE_PASSWORD__REQUESTED';
export const CHANGE_PASSWORD__SUCCEEDED = 'CHANGE_PASSWORD__SUCCEEDED';
export const CHANGE_PASSWORD__FAILED = 'CHANGE_PASSWORD__FAILED';

// etc

As we are dispatching a new action we have two potential options:

  • handle via another saga function
  • handle via a reducer

Well, we still have more work to do with the response body, so the next step here is going to be another saga action:

export function *doChangePasswordFailed(action) {

  console.log('doChangePasswordFailed', action);
}

export function *watchChangePasswordFailed() {
  yield *takeLatest(types.CHANGE_PASSWORD__FAILED, doChangePasswordFailed);
}

And don't forget to add this new watch function to your root saga:

// /src/sagas/index.js

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

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

    fork(authSaga.watchLogoutRequested),

    fork(profileSaga.watchChangePassword),
    fork(profileSaga.watchChangePasswordSucceeded),
    fork(profileSaga.watchChangePasswordFailed),
    fork(profileSaga.watchRequestProfile),
  ];
}

If you submit the change password form now with a bit of bad data, you should see the console log statement. Great.

Now, as we are using Redux Form, adding an error against any particular form field is fairly straightforward.

Firstly, we need to tell Redux Form that we wish to stop the form submission process. Redux Form provides us with the stopSubmit action creator for this very purpose.

stopSubmit(form:String, errors:Object)

// Flips the submitting flag false and populates submitError for each field.

As shown in the function signature for stopSubmit, we need to pass in two bits of data.

We need to tell stopSubmit the name of the form for which it should stop the submission process. It would make sense to start using constants here for your form names, as it makes life much easier. But you don't need too, a simple string is all that's required.

We also need to pass in an object containing keys that match our form field names, and the value of which will become the error string.

It sounds a little more complicated than it truly is:

import {stopSubmit} from 'redux-form';

export function *doChangePasswordFailed(action) {

  console.log('doChangePasswordFailed', action);

  yield put(stopSubmit('change-password', {
    currentPassword: 'Your current password was incorrect',
    newPassword: 'Awww, epic fail',
  }));
}

export function *watchChangePasswordFailed() {
  yield *takeLatest(types.CHANGE_PASSWORD__FAILED, doChangePasswordFailed);
}

Ok, so change-password is the name of our form:

// /src/components/ChangePasswordForm.js

// * snip *

export default reduxForm({
  form: 'change-password'
})(ChangePasswordForm);

Again, constants start to make more sense here.

And then for example, currentPassword is the name of our field that - you guessed it - allows a user to enter their current password:

// /src/components/ChangePasswordForm.js

const ChangePasswordForm = (props) => {
  return (
    <form onSubmit={props.handleSubmit} className="form-change-password">

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

The name field is what matters here.

Now, none of this will actually work unless the component you are using for your Field actually has some way of displaying the errors. If you are using the most basic component type (a simple string: input), then you won't see much.

Instead, it starts to make more sense to define our own re-usable FormField component:

// /src/components/FormField.js

import React from 'react';
import classNames from 'classnames';

const formField = ({ input, label, type, meta: { touched, error } }) => {

  const formGroup = classNames(
    'form-group',
    {'has-danger': touched && error}
  );

  const formControlCss = classNames(
    'form-control',
    {'form-control-danger': touched && error}
  );

  return (
    <div className={formGroup}>
      <label className="form-control-label">{label}</label>
      <div>
        <input {...input} placeholder={label} type={type} className={formControlCss}/>
        {touched && error && <span className="form-control-feedback">{error}</span>}
      </div>
    </div>
  );
};

formField.propTypes = {
  input: React.PropTypes.object.isRequired,
  meta: React.PropTypes.object.isRequired,
  type: React.PropTypes.string.isRequired,
  label: React.PropTypes.string
};

export default formField;

Note in particular the line:

{touched && error && <span className="form-control-feedback">{error}</span>}

In other words, only show this if the field is touched (think: blurred), and the error string is true (think: there is an error). The inclusion of the span as the third part of this expression is as typical React idiom. The string will only be shown if the previous two checks return true.

Switch out the input component type for the new FormField:

// /src/components/ChangePasswordForm.js

import FormField from './FormField';

const ChangePasswordForm = (props) => {
  return (
    <form onSubmit={props.handleSubmit} className="form-change-password">

      <Field component={FormField}
             name="currentPassword"
             type="password"
             label="Current Password"
             placeholder="Current Password"
             required="required"
             className="form-control"
      />

At this point you should be able to submit the form and see your hardcoded errors mapped onto the respective fields. Any fields missing from the errors object will be considered to not have errors. Lovely.

This addresses the first of our three issues.

In reality we will want to display the response returned by the API, and also fix the third problem with the password fields. But for now, this is a good start.


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