Keeping your ES6 JavaScript code simple

leosl

Leonardo Lima

Posted on February 5, 2018

Keeping your ES6 JavaScript code simple

After a few years working almost exclusively with Ruby on Rails and some jQuery, I changed my focus to front-end development and discovered the beauties of JavaScript ES6 syntax and the exciting modern libraries such as React and Vue. I started to implement new features using nothing but ES6 Vanilla JS and instantly fell in love with the class abstraction and those arrow sweeties functions.

Nowadays, I'm generating large amounts of JS code, but, since I'm a padawan, there's yet a lot of room for improvement. Through my studies and observations, I learned that even using syntactic sugars featured in ES6, if you don't follow the main principles of SOLID, your code has a high chance to become complex to read and maintain.

To demonstrate what I'm talking about, I'll walk you through one fantastic Code Review session I had last week. We are going to start with a 35-lines JS Class and will finish with a beautiful 11-lines code piece using only slick functions!

refactoring

With patience and resilience, you will be able to observe and apply the pattern to your own codebase.

The feature

What I needed to accomplish was quite of simple and trivial: get some information from the page and send a request to a third-party tracking service. We were building an event tracker and tracking some pages along with it.

The code examples below implement the same task using different code design tactics.

Day 1 - Using ES6 Class syntax (aka Object Prototype Pattern wrapper)

Filename: empty-index-tracking.js

import SuccessPlanTracker from './success-plan-tracker';
import TrackNewPlanAdd from './track-new-plan-add';

class EmptyIndexTracking {
  constructor(dataset) {
    this.trackingProperties = dataset;
    this.emptyIndexButtons = [];
  }

  track(element) {
    const successPlanTracker = new SuccessPlanTracker(this.trackingProperties);
    const emptyIndexProperty = {
      emptyIndexAction: element.dataset.trackingIdentifier,
    };

    successPlanTracker.track('SuccessPlans: EmptyIndex Interact', emptyIndexProperty);
  }

  bindEvents() {
    this.emptyIndexButtons = Array.from(document.getElementsByClassName('js-empty-index-tracking'));

    this.emptyIndexButtons.forEach((indexButton) => {
      indexButton.addEventListener('click', () => { this.track(indexButton); });
    });
  }
}

document.addEventListener('DOMContentLoaded', () => {
  const trackProperties = document.getElementById('success-plan-tracking-data-empty-index').dataset;

  new EmptyIndexTracking(trackProperties).bindEvents();
  new TrackNewPlanAdd(trackProperties).bindEvents();
});

export default EmptyIndexTracking;
Enter fullscreen mode Exit fullscreen mode

You can notice above that I started smart isolating the generic tracker SuccessPlanTracker to be reused in another page besides the Empty Index. But, wait a minute. If this is the empty index tracker, what on earth this foreigner TrackNewPlanAdd was doing there?

Day 2 - (Code Review begins) - Getting rid of Class boilerplate code

Filename: bind-empty-index-tracker.js

import SuccessPlanTracker from './success-plan-tracker';

let emptyIndexButtons = [];
let emptyIndexTrackingData = {};
let emptyIndexActionProperty = {};
let emptyIndexTrackingProperties = {};

const trackEmptyIndex = (properties) => {
  const successPlanTracker = new SuccessPlanTracker(properties);
  successPlanTracker.track('SuccessPlans: EmptyIndex Interact', properties);
};

const populateEmptyIndexData = () => {
  emptyIndexButtons = document.querySelectorAll('.js-empty-index-tracking');
  emptyIndexTrackingData = document.getElementById('success-plan-tracking-data-empty-index').dataset;
};

const bindEmptyIndexTracker = () => {
  populateEmptyIndexData();
  emptyIndexButtons.forEach((indexButton) => {
    indexButton.addEventListener('click', () => {
      emptyIndexActionProperty = { emptyIndexAction: indexButton.dataset.trackingIdentifier };
      emptyIndexTrackingProperties = { ...emptyIndexTrackingData, ...emptyIndexActionProperty };
      trackEmptyIndex(emptyIndexTrackingProperties);
    });
  });
};

export default bindEmptyIndexTracker;
Enter fullscreen mode Exit fullscreen mode

Okay, now the file name is clearly reflecting the feature responsibility and, look at that, there is no more EmptyIndexTracker class (less boilerplate code - learn more here and here), we are using simple functions variables and, man, you're even using those shining ES6 Object Spread dots!

The querySelectorAll method already returns an array so we were able to remove the Array.from() function from Array.from(document.getElementsByClassName('js-empty-index-tracking')) - remember that getElementsByClassName returns an object!

Also, since the central responsibility is to bind HTML elements, the document.addEventListener('DOMContentLoaded') doesn't belongs to the file anymore.

Good job!

Day 3 - Removing ES5 old practices and isolating responsibilities even more

Filename: bind-empty-index.js

import successPlanTrack from './success-plan-tracker';

export default () => {
  const buttons = document.querySelectorAll('.js-empty-index-tracking');
  const properties = document.getElementById('success-plan-tracking-data-empty-index').dataset;

  buttons.forEach((button) => {
    properties.emptyIndexAction = button.dataset.trackingIdentifier;
    button.addEventListener('click', () => {
      successPlanTrack('SuccessPlans: EmptyIndex Interact', properties);
    });
  });

  return buttons;
};
Enter fullscreen mode Exit fullscreen mode

If you pay close attention, there is no SuccessPlanTracker class in the code above, the same fate of the old EmptyIndexTracker. The class-killing mindset once installed spreads and multiplies itself. But don't fear, my good lad! Remember, always try to keep your JS files simple: since there is no need to know about the states of class instances and the classes were exposing practically only one method, don't you think using the ES6 class abstraction was a little bit overkilling?

Did you notice that I removed the variables instances from the top of the file? This practice remounts to ES5 and we don't need to worry so much about it now that we have ES6+ syntax!

Finally the last major change in the third version: our empty index tracker binder now does only one thing: elements binding! Following those steps brought the code very close to the Single Responsibility Principle - one of the most important SOLID principles.

Day 4 - (Code review ends) - Avoiding DOM sloppy manipulation

import successPlanTrack from './tracker';

const trackAction = (properties, button) => {
  const trackProperties = { ...properties, emptyIndexAction: button.dataset.trackingIdentifier };
  successPlanTrack('SuccessPlans: EmptyIndex Interact', trackProperties);
};

export default () => {
  const buttons = document.querySelectorAll('.js-empty-index-tracking');
  const dataset = document.getElementById('success-plan-tracking-data-empty-index').dataset;
  const properties = { ...dataset, emptyIndexAction: '' };

  buttons.forEach(button => (
    button.addEventListener('click', () => trackAction(properties, button))
  ));

  return buttons;
};
Enter fullscreen mode Exit fullscreen mode

Hey, there are more lines now, you liar!

The thing is that our third version was a little broken. We were inappropriately mutating DOM Elements datasets in the line properties.emptyIndexAction = button.dataset.trackingIdentifier;. The property of one button was being passed to another button, generating messed up tracking events. To resolve this situation, we removed the responsibility of assigning the emptyIndexAction property from the binding loop to a proper function by creating its own scoped method trackAction().

By adding those extra lines, we improved our code following the good principles of single responsibility and encapsulating.


Finally, to wrap up and write down:

  • If you want to design and write marvelous pieces of code, you need to be willing to explore further and go beyond the limits of a proper and modern syntax.
  • Even if the first version of your code ended up being very simple and readable, it doesn't necessarily mean that the system has a good design or that it follows at least one of the SOLID principles.
  • It's also essential to accept constructive code reviews and let other developers point down what you can do better. Remember: to keep your code simple you need to think bigger.

ProTip to-go: Here's a very useful ES6 cheatsheet


Thank you very much for reading the article. Have another refactoring examples or code review lessons to share? Please drop a comment below! Also, you can help me share this message with others by liking and sharing it.

PS: A big thanks to @anderson06 for being such a good code 'pal giving me awesome feedbacks at code review sessions.

💖 💪 🙅 🚩
leosl
Leonardo Lima

Posted on February 5, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

JavaScript Function Default Argument
javascript JavaScript Function Default Argument

September 17, 2022

Keeping your ES6 JavaScript code simple
javascript Keeping your ES6 JavaScript code simple

February 5, 2018