Registration - Part 3


In this final video in this series we are going to fix up the two remaining problems with our Registration journey. These are that we do not see any indication of success, when the registration goes well. And that we do not see any errors, when the registration fails for any reason.

We can start by fixing the easier of the two - the successful, or happy path.

The process we will follow here is to dispatch a specific action on a successful registration. We will then have another function which listens for (or watch'es) for this particular action type.

Let's start by adding in the new dispatch:

// /src/sagas/register.saga.js#L13

export function *doRegistration(action) {

  try {

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

    const {username, email, newPassword, newPasswordRepeated} = action.payload;

    const {msg, token} = yield call(api.register, username, email, newPassword, newPasswordRepeated);

    // this part is new
    yield put({
      type: types.REGISTER__REQUEST__SUCCEEDED,
      payload: {
        token,
        message: msg
      }
    });

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

  }

}

We've added in the new dispatch, making sure that we dispatch an action of type REGISTER__REQUEST__SUCCEEDED, along with the token, and re-keyed the msg to message, which I prefer.

At this point, this action is simply dispatched and nothing hears about, or takes any action upon it.

Let's fix that by adding in a new watch function:

// /src/sagas/register.saga.js#L13

export function *doRegistrationSuccess(action) {

}

export function *watchRegistrationSuccess() {
  yield* takeLatest(types.REGISTER__REQUEST__SUCCEEDED, doRegistrationSuccess);
}

We must also remember to add in the new watchRegistrationSuccess function to our rootSaga:

import {fork} from 'redux-saga/effects';
import * as authSaga from './auth.saga';
import * as profileSaga from './profile.saga';
import * as registerSaga from './register.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),

    fork(registerSaga.watchRequestRegistration),
    fork(registerSaga.watchRegistrationSuccess),
  ];
}

Now we hear about the action, and then call doRegistrationSuccess with that action, but that function does nothing.

Where this gets more interesting is in how we now handle logging the user in. As we know, a successful registration response contains a JWT - the token. Simply receiving and storing this token won't actually log us in.

You may now be thinking that we need to implement a bunch of new logic - a specific case where we handle login differently if it's a post-registration login, versus the more typical returning user type of login.

Not so :) We can simply dispatch a new LOGIN__SUCCEEDED action with the token, and be magically (well, not magically - there is no magic at all here) logged in properly. Awesome.

One extra benefit of this is that our LOGIN__SUCCEEDED action handler already handles the redirection. Super cool. Tons of hard work already done.

We may also want to hook up a notification system to show a little Toast notification if the user is logged in. If this interests you, check out this branch where I added in a notification system.

Adding a notification, and starting the login succeeded process can both occur at the same time, so we use the yielded array syntax to process both of these actions in parallel. This is covered in more detail in a previous video.

export function *doRegistrationSuccess(action) {
  yield [
    put({
      type: types.ADD_NOTIFICATION,
      payload: {
        message: action.payload.message,
        level: 'success'
      }
    }),
    put({
      type: types.LOGIN__SUCCEEDED,
      payload: {
        idToken: action.payload.token
      }
    })
  ];
}

export function *watchRegistrationSuccess() {
  yield* takeLatest(types.REGISTER__REQUEST__SUCCEEDED, doRegistrationSuccess);
}

So that handles the happy path.

Now onto the unhappy path.

When Things Go Wrong

Most of my time is spent handling problems. The happy path is usually the easier of the two. There is one good path, and often several potential problem paths.

Fortunately, we have put in all of the foundations required to handle the unhappy paths.

We have our form error helper function to process the returned Symfony form errors into a nice, usable bit of JavaScript.

We have our form components which have both error styling, and a place to display error messages.

Now we just need to hook up the "wires", and everything should "just work (tm)". Good Lord, it's like being in the Apple store.

Let's add in a new watch function for our failure path:

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

// * snip *

export function *doRegistrationFailed(action) {

}

export function *watchRegistrationFailed() {
  yield* takeLatest(types.REGISTER__REQUEST__FAILED, doRegistrationFailed);
}

And remember to udpate the rootSaga accordingly:

// /src/sagas/index.js

import {fork} from 'redux-saga/effects';
import * as authSaga from './auth.saga';
import * as profileSaga from './profile.saga';
import * as registerSaga from './register.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),

    fork(registerSaga.watchRequestRegistration),
    fork(registerSaga.watchRegistrationSuccess),
    fork(registerSaga.watchRegistrationFailed),
  ];
}

We need to dispatch a new action when things go wrong.

Let's add that in also:

export function *doRegistration(action) {

  try {

    // * snip *

  } catch (e) {

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

    // specific
    yield put({
      type: types.REGISTER__REQUEST__FAILED,
      payload: {
        response: e.response,
      }
    });

  } finally {

And so now if things go wrong, we dispatch a very specific action containing the converted JSON response from our API, containing the form errors as we have already discussed in previous videos.

Knowing this, we can extract each of these form errors, and call stopSubmit of our Redux Form submission, passing in the processed error data:

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

// * snip *

export function *doRegistrationFailed(action) {
  const errorData = action.payload.response;

  const [username, email, newPassword, newPasswordRepeated] = [
    yield call(formErrorHelper, errorData, 'children.username.errors'),
    yield call(formErrorHelper, errorData, 'children.email.errors'),
    yield call(formErrorHelper, errorData, 'children.plainPassword.children.first.errors'),
    yield call(formErrorHelper, errorData, 'children.plainPassword.children.second.errors')
  ];

  yield put(stopSubmit('registration-form', {
    username,
    email,
    newPassword,
    newPasswordRepeated
  }));
}

export function *watchRegistrationFailed() {
  yield* takeLatest(types.REGISTER__REQUEST__FAILED, doRegistrationFailed);
}

And with that, our registration journey should now be complete.

We have now implemented the foundations of a basic web application including user registration, login, and profile management.

Along the way you have learned the repeatable formula for adding most any new page into your application. We've covered forms, a critical part of any modern web application. And also sending this form data off to our back end API.

I hope you've found this course to be useful. As mentioned, there is an alternative branch available that contains a notification system - something not covered directly in these videos, but is useful all the same.

As ever, if you have any comments, questions, feedback or suggestions, please do feel free to leave them in the comments below - or for any video where the comment is most relevant.

Code For This Course

Get the code for this course.

Episodes