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!
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;
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;
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;
};
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;
};
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.
Top comments (1)
nice implementation, im learning DOM with ES6 too, your post is usefull to me