Leonardo Lima
28 Feb 2018
•
4 min read
After a few years working almost exclusively with Ruby on Rails and some jQuery, I changed my focus to front-end development. I 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 JavaScript, and fell instantly in love with all this new class abstraction and those arrow functions.
Nowadays, I’m generating large amounts of JavaScript code. But, since I consider myself a JavaScript Padawan, there’s yet a lot of room for improvement. Through my studies and observations, I’ve learned that even with the use of syntactic sugars featured in ES6, if you don’t follow the main principles of SOLID, your code has a high chance of becoming 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 with with a buddy of mine. We are going to start with a 35-lines JavaScript 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.
What I needed to do was quite 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 on the client side and tracking some page actions along with it.
The code examples below implement the same task using different code design tactics.
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 an empty index tracker, what on earth is this foreigner TrackNewPlanAdd
doing there?
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 clearly reflects the feature responsibility and, look at that, there is no more EmptyIndexTracker
class giving us less boilerplate code. Learn more here and here. We are using simple functions variables and, man, what a good catch using those shining ES6 Object Spread dots.
I find out that the querySelectorAll method already returns an array, so we are able to remove the Array.from()
function from Array.from(document.getElementsByClassName('js-empty-index-tracking'))
Remember that getElementsByClassName method returns an object.
Also, since the central responsibility is to bind HTML elements, the document.addEventListener('DOMContentLoaded')
function invocation doesn’t belongs to the file anymore.
Good job!### Day 3 — Removing ES5 old practices and isolating responsibilities even more.
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 — it suffered the same fate as 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 JavaScript files simple. 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 of overkill?
Did you also 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 this 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.
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 solve 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, better encapsulating each action.
ProTip to-go: Here’s a very useful ES6 (ES2015+) cheatsheet
Find more from the author Leonardo Lima at his portfolio or his twitter
If you’re passionate about Front End development, check out the JavaScript Works job-board here.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!