Debugging Redux with ES2015 Proxies

by Alec Barlow


Christopher asks the following:

Hi, I found it quite tedious exporting and importing all those type consts, so did this instead:

export const ActionTypes = {
 EmailChanged: 'email_changed',
 PasswordChanged: 'password_changed',
 LoginUserStarted: 'login_user_started',
 LoginUserSuccess: 'login_user_success',
 LoginUserFail: 'login_user_fail',
 EmployeeUpdate: 'employee_update'
};
import { ActionTypes } from './types.js'; 

export const emailChanged = (text) => ({
 type: ActionTypes.EmailChanged,
 payload: text
 });

Any downside to that?

I really like this idea. If we end up with the same functionality with less code redundancy, great! However, doing this may create some unintended consequences, making the app more difficult to debug.

For example, let's consider of the most common problems that I run into when using Redux: trying to figure out why an action is not being captured by a reducer. For someone just getting starting with Redux, debugging this issue can be especially overwhelming because of how Redux manages data flow.

In any application that I have built, most bugs that I have run into are simply due to typos. However, the solution to this particular problem is harder to spot because no errors are raised when the application is run. Take a look at the snippet below.

// types.js

export const ActionTypes = {
 EmailChanged: 'email_changed',
 PasswordChanged: 'password_changed',
 LoginUserStarted: 'login_user_started',
 LoginUserSuccess: 'login_user_success',
 LoginUserFail: 'login_user_fail',
 EmployeeUpdate: 'employee_update'
};


// authReducer.js

import {
  ActionTypes
} from '../actions/types';

const authReducer = (state = {}, action) => {
  switch (action.type) {
    case ActionTypes.LoginUserSucess:
      return { ...state, user: action.payload };
    default:
      return state;
  }
}

export default authReducer;

Assuming we dispatched an action with type LoginUserSuccess, the authReducer should catch the action before the default case is returned. But what if that is not happening? Where do we start the debugging process. There does not appear to be anything wrong with the code in the reducer; the action type was imported and matches the case in the switch statement. There are no errors in the browser. Where is the issue?

You may have noticed that I misspelled success in authReducer.js, but the reason this can be hard to catch is because undefined cases do not cause an error. When we define case ActionTypes.LoginUserSucess:, its value is actually undefined, so our reducer always hits the default case.

Now, there are tools out there to help us catch this. Unfortunately, linters really will not help here; creating a custom rule to disallow undefined object properties will create other problems for us. Using Flow or TypeScript will solve the problem, but many projects do not use them. Also, if you are new to React, adding in Flow or switching to TypeScript probably is not on your radar.

Enter Proxies

Proxies are a feature of ES2015 that allow us to customize operations on a object. They can be used in many different ways, and you can find some useful examples here and here. For our problem, this example from Mozilla looks promising:

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception

If proxies can be used to validate that properties assigned to an object are of a certain type and value, we can definitely use it to ensure that our action types are never undefined. Let’s refactor our types.js file.

// types.js

const ActionTypes = {
 EmailChanged: 'email_changed',
 PasswordChanged: 'password_changed',
 LoginUserStarted: 'login_user_started',
 LoginUserSuccess: 'login_user_success',
 LoginUserFail: 'login_user_fail',
 EmployeeUpdate: 'employee_update'
};

const typeValidator = {
  get(obj, prop) {
    if (obj[prop]) {
      return prop;
    } else {
      throw new TypeError(`${prop} is not a valid action type`);
    }
  }
};

module.exports = new Proxy(ActionTypes, typeValidator);

First, we define a object containing all our action types. Then we define our validator handler typeValidator. The get() method inside our handler is called a trap, and provides access to the properties of a object. If the property we are looking for, an action type, in this case, exists in ActionTypes, return that prop, unmodified. Otherwise, throw an error because the prop does not exist.

Finally, export a new proxy, passing ActionTypes as the target and typeValidator as the handler. However, it is important to note that the ES2015 module system does not work well with proxies, so module.exports and require() must be used for exporting and importing the types.

Barely any code needs to change in the reducer and action creator files, but in order for the action types to be imported successfully, we just need one of code in a new file:

// typesProxy.js

export const ActionTypes = require('./types');

// Also, in the reducer and action creator files,
// change the import path '/types' to
// '/typesProxy'

By creating a proxy to verify the existence of an action type, we no longer have to worry about incorrectly naming it because an error will be thrown in the browser console as soon as the application starts:
Uncaught TypeError: LoginUserSucess is not a valid action type

Reduce the headaches you get when developing an application using Redux and start using proxies.

Previous Post Next Post