Part of a series to track the 90-Day Beat Down
I recently got hired for a new gig. They hired 2 of us on a 90-day prove-yourself contract. Pretty sure this is a "competition" of sorts to see which of us can rise to the top.
This is my way of internalizing what I learn along the way.
Shore Up the Foundation with JavaScript
Callbacks and Closure Patterns
- Sometimes javascript apis are inconsistent in how they act. By wrapping them in functions, we have more control, more customization, and stronger callbacks...
let createTimeout = (time) => {
setTimeout(() => {
console.log('done');
}, time)
};
createTimeout(100);
This mostly works just fine, but with small amounts of time, it becomes increasingly difficult to know if the timeout is actually occurring. Let's wrap it in another fatarrow function:
let createTimeout = (time) => () => {
setTimeout(() => {
console.log('done');
}, time);
};
let timeout1s = createTimeout(1000);
let timeout2s = createTimeout(2000);
let timeout3s = createTimeout(3000);
timeout1s();
timeout2s();
timeout3s();
^^^ that code provides us with more customization. But it does not give us more control over what happens within setTimeout. What if we wanted to differentiate the callback behavior based on the time parameter? That would mean our code needed to act one way for 1s, a different way for 2s, etc.
let createTimeout = (time) => (callback) => {
setTimeout(callback, time);
};
let timeout1s = createTimeout(1000);
let timeout2s = createTimeout(2000);
let timeout3s = createTimeout(3000);
// call the functions AND provide unique callbacks
timeout1s(() => {
console.log("one");
});
timeout2s(() => {
console.log("two");
});
timeout3s(() => {
console.log("three");
});
- This puts us on track to deal with async behavior more consistently. But a huge part of async behavior is building in an exit strategy. "What if we need to cancel mid-function?"
let createTimeout = (time) => (callback) => {
// setTimeout returns an id we can use
let id = setTimeout(callback, time);
// return that from our function so we can access it
return id;
};
let timeout1s = createTimeout(1000);
let timeout2s = createTimeout(2000);
let timeout3s = createTimeout(3000);
let id1s = timeout1s(() => {
console.log("one");
});
id1s now holds the value of the setTimeout id that was created, and we can clear it with clearTimeout(id1s)
.
But if we want even more control over our code (which we do), we can actually continue to wrap our functionality inside more functions!
let createTimeout = (time) => (callback) => {
let id = setTimeout(callback, time);
// returns a function to capture **behavior**
return () => clearTimeout(id1s);
};
let timeout1s = createTimeout(1000);
let timeout2s = createTimeout(2000);
let timeout3s = createTimeout(3000);
let cancel1s = timeout1s(() => {
console.log("one");
});
cancel1s();
timeout2s(() => {
console.log("two");
});
The big picture concepts I'm trying to capture (and use moving forward) are:
- You increase code flexibility by returning functions from within functions
- You increase flexibility and customization by passing functions in to other functions
Let's go through the same process with an event listener
End goal:
let addListener = selector => eventType => listener => {
let element = document.querySelector(selector);
element.addEventListener(eventType, listener);
return () => {
element.removeEventListener(eventType, listener);
}
};
let addButtonListener = addListener('button');
let addButtonClickListener = addButtonListener("click")
let removeBtnClickListener = addButtonClickListener(() => {
console.log('button clicked');
})
// removeBtnClickListener();
How did we get there?
- Each piece of the function
addListener
is returning another function. - Anytime we invoke a piece of that chain, we're getting a function returned to us in addition to the function being executed.
Let's break it down:
let addListener = selector => eventType => listener => {
let element = document.querySelector(selector);
element.addEventListener(eventType, listener);
return () => {
element.removeEventListener(eventType, listener);
}
};
when we call addListener('button');
what do we have?
let addButtonListener = addListener('button');
console.log(addButtonListener);
let addButtonClickListener = addButtonListener("click")
console.log(addButtonClickListener);
let removeBtnClickListener = addButtonClickListener(() => {
console.log('button clicked');
});
console.log(removeBtnClickListener);
Those logs in the console are pretty informative:
Each thing logged is a function! and the first two return another function! We now have control over how addEventListener interacts with other APIs, like setTimeout, gives us confidence in our pattern moving forward
Place this code in a js file, add that file into a simple html with 1 btn, and see what happens:
// index.html
<!DOCTYPE html>
<html lang="en">
<body>
<button id="button">Click My Dick</button>
<script src="./index.js" type="text/javascript" />
</body>
</html>
and
//index.js
let addButtonListener = addListener('button');
let addButtonClickListener = addButtonListener("click")
let removeBtnClickListener = addButtonClickListener(() => {
console.log('button clicked');
})
// removeBtnClickListener();
If the remove call is commented out, we see the clicks log to the console. With the remove un-commented, the listener is removed before we (the user) ever have a chance to click anything.
Always check devTools to confirm these things. console.log can only take you so far and is annoying at best):
Just to reiterate the goal of this review:
Top comments (1)
Amazing topic! That was really good. As a beginner, I understood a lot of things. Great explanation!