DEV Community

Cover image for JS Function Wrapping
Drew
Drew

Posted on • Updated on

JS Function Wrapping

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

^^^ 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");
}); 
Enter fullscreen mode Exit fullscreen mode
  • 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");
});
Enter fullscreen mode Exit fullscreen mode

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");
});

Enter fullscreen mode Exit fullscreen mode

The big picture concepts I'm trying to capture (and use moving forward) are:

  1. You increase code flexibility by returning functions from within functions
  2. 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();
Enter fullscreen mode Exit fullscreen mode

How did we get there?

  1. Each piece of the function addListener is returning another function.
  2. 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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Those logs in the console are pretty informative:
console logs

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>
Enter fullscreen mode Exit fullscreen mode

and

//index.js
let addButtonListener = addListener('button');

let addButtonClickListener = addButtonListener("click")

let removeBtnClickListener = addButtonClickListener(() => {
    console.log('button clicked');
})

// removeBtnClickListener();
Enter fullscreen mode Exit fullscreen mode

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):

click even vs none

Just to reiterate the goal of this review:

Use functions to return other functions. Also, pass functions into other functions. This function wrapping will provide more code control, flexibility, and even reusability,

Top comments (1)

Collapse
 
slex1onemusdy profile image
Mustafa

Amazing topic! That was really good. As a beginner, I understood a lot of things. Great explanation!