« Previous Tutorial Next Tutorial »

Welcome back to Five Minute React. We’ve had a little to-do note in our account reducer for a while now that says “handle errors”. We’re going to start in on that today, although the truth is we won’t be handling them in that reducer, but in their own separate reducer. One of the core concepts of developing with Redux is keeping your state split into manageable chunks, and anyway it makes no sense to keep our error state in the “authentication” part of the Store, since eventually many other parts of our app could throw errors that need to be reported.

Instead, we’re going to make “error” a top-level part of the Store, like “authentication” and “progress”. This will also show off a cool feature of Redux: more than one reducer can listen for the same action, and then manipulate different parts of the Store accordingly. So while our account reducer is doing one thing on, say, AUTHENTICATION_LOGIN_FAILURE, our error reducer can be doing something else, and our application state will change in two places at once.

If all this sounds confusing, don’t worry, we’ll step through it. It’s all stuff we’ve already done, and while it’s going to touch a lot of files, most of it will be minor tweaks and additions. Let’s get started.

Create a new file in /src/actions called error.js and fill it with the following lines:

// Action Creators
export const clearError = () => ({ type: 'ERROR_CLEARED' });

ESLint will complain about having no default export here, but you can ignore that. It’s likely at a future date we’ll be adding more action creators to this file, so we’re sticking with explicit exports here. For today, there’s nothing else to do, so save the file, and create a new file in /src/reducers also called error.js. This one’s a bit bigger. We’re going to watch for three authentication actions, plus the new one we just defined in the previous file. Here’s the code:

const initialState = {
  isError: false,
  error: {},
};
export default function reducer(state = initialState, action) {
  switch (action.type) {
    case 'AUTHENTICATION_LOGIN_FAILURE':
    case 'AUTHENTICATION_LOGOUT_FAILURE':
    case 'AUTHENTICATION_REGISTRATION_FAILURE': {
      const newState = Object.assign({}, initialState);
      newState.isError = true;
      newState.error = action.error;
      return newState;
    }
    case 'ERROR_CLEARED': {
      const newState = Object.assign({}, initialState);
      return newState;
    }
    default: {
      return state;
    }
  }
}

As you can see, for now we’re only going to handle one error at a time (which is why our error parameter in the initial state is an object and not an array). If the app throws multiple errors at once, the user will only see the last one thrown. There are some applications for which this would be suboptimal, and you’d want to display multiple errors to the user, but we’re doing this for two reasons. First, it keeps things simple – we can always build on this later. Second, the app’s not complex enough right now that it’s likely to throw multiple errors at once.

Save this file. Open /src/reducers/authentication.js and delete line 37:

      // todo: handle error!

Since we are, in fact, handling the error. Just not here. Save that file, and move on to /src/actions/authentication.js. We need to clean a couple of things up, here, so change line 78 to:

        dispatch(loginFailure(new Error('Email or Password Incorrect. Please Try again.')));

And then change line 110 to:

        dispatch(logoutFailure(new Error(response.status)));

And line 114 to:

      dispatch(logoutFailure(new Error(error)));

Finally, change line 153 to:

        dispatch(registrationFailure(new Error('Registration Failed. Please try again.')));

And we’re set. Now we’re consistently sending JavaScript Error objects out when problems happen, which is good. Save this file, and open up /src/reducers/index.js. We need to add our error reducer to all of the reducers being bundled up for the Store, so below line 2, add the following:

import ErrorReducer from '../reducers/error';

And below line 7, add this code:

  error: ErrorReducer,

Looking good. Save the file, and we’re done wiring up the guts of our error reporting system. Now we need to focus on display! To do that, we’re going to create an ErrorBox component and a container for it, and connect the container so that we can make the error box closeable (by firing that ERROR_CLEARED action we just set up).

Let’s start with the error box itself. Create a file in /src/components/shared called ErrorBox.jsx. We can go pure function here, so the code’s pretty straightforward. Here it is:

import React from 'react';
import { Alert } from 'reactstrap';

export default function ErrorBox(props) {
  const { closeErrorFunction } = props;
  const { error, isError } = props.errorStore;
  return (
    <div className="row justify-content-center">
      <div className="col-6">
        <Alert color="danger" isOpen={isError} toggle={closeErrorFunction}>
          <strong>Error:</strong> {error.message}
        </Alert>
      </div>
    </div>
  );
}

As you can see, we’re going to need to provide some props for this function to use. We’ll do that in the container, so save this file and then create a new file, also in /src/components/shared called ErrorBoxContainer.jsx. We need to use a class here so we can define a method to pass down for clearing the error box. It’s still a pretty short file. Feeling up to just looking at the whole thing at once? I’m confident you can handle it. Here it is:

import React from 'react';
import { connect } from 'react-redux';
import { clearError } from '../../actions/error';

import ErrorBox from './ErrorBox';

class ErrorBoxContainer extends React.Component {
  constructor(props) {
    super(props);

    this.closeError = this.closeError.bind(this);
  }

  closeError() {
    const { dispatch } = this.props;
    dispatch(clearError());
  }

  render() {
    const { errorStore } = this.props;
    return (
      <ErrorBox errorStore={errorStore} closeErrorFunction={this.closeError} />
    );
  }
}

function mapStateToProps(state) {
  return { errorStore: state.error };
}

export default connect(mapStateToProps)(ErrorBoxContainer);

Real quick, here’s what we’re doing. We’re grabbing our error state from the Store (which, because we’re using connect, also gives us access to dispatch). Then we create a small function to dispatch our clearError action creator, which we’ve imported from the action file we created earlier. Finally, we call an instance of ErrorBox and pass it the information from the Store, and the function, which it will use to display information and allow users to dismiss the error if they’ve fixed the problem … or, I suppose, if they’re just like “whatever, man, who cares?”

Save this file. We’ve got one last thing to do, and that’s call our error box somewhere. I suggest at the top level of the template, because that way it’ll be available on every page. To do that, open up /src/Template.jsx. Underneath line 3, import our container with this code:

import ErrorBox from './shared/ErrorBoxContainer';

and then, under line 18, add the following:

          <ErrorBox />

This is … not the most complicated part of the tutorial! You’re done. Save the file, and let’s go test. Switch to a browser, log out if you’re logged in, and then head for the log-in page. Put some nonsense in there and hit the button. Voila! You can has error handling. Nice job. This system’s not perfect and we’ll probably expand on it in the future, but it’s a good step.

Next up, form validation, a subject which just about everyone hates, but not as much as they hate answering customer-support requests from folks who didn’t realize that the “email” field was supposed to contain a valid email address!

See you next time.

« Previous Tutorial Next Tutorial »