Callbacks - The Definitive Guide

When I worked as a Full-Stack developer, creating new modules using native Javascript (hereafter JS), and dealing with issues such as supporting the existing system gave me invaluable knowledge in practice. This has greatly helped me to understand what many of the Javascript constructs do in practice. From this article, we will share with you many of our experiences not only on the .NET platform but also on Javascript. These articles will give you a better understanding of the internal structure of JS.

function add(a, b) {
    return a + b;
}

This JS article will be about callbacks, which many people struggle with. Callbacks are used a lot in practice. Even if you use a JS library, one of the most common JS topics you will use will be callbacks.

It all starts with functions….

The theme of functions in programming languages allows us to create reusable blocks of code. When using functions, you can ensure that a block of code is reused by passing parameters to them.

Here, if you give any value instead of arguments a and b, the program performs the correct calculation. With this approach, a kind of formula concept in mathematics is formed, and we get rid of repeatedly writing operations from the same classification. However, there are forms of functions to which it is not enough to simply pass a variable parameter. Sometimes it is necessary to pass blocks of code into smaller chunks to enable reuse and to handle this dynamically by passing one function as an argument to another function.

But what are the callbacks?

The callback is the process of passing another function as an argument to a function. Why would a function need to pass another function, or what does a callback do?

To answer this question, let's look at the simplest array search algorithm. Let's take a step-by-step look at how to perform an array search without resorting to JS's default search function.

Task: Given an array of numbers 45,56,103,91,405, 324. We need to find numbers greater than 50 in this array.

let array = [45, 56, 103, 91, 405, 324];

function findInArray(arr) {
    let result = [];
    for (let f of arr) {
        if (f > 50) {
            result.push(f);
        }
    }
    return result;
}

After a while, it is necessary to search for elements in another part of the program on the same array (or it can be another array) with another condition (for example, finding numbers greater than 100 and less than 200). According to what we learned about this in algorithms, the code we write must respect the massiveness property of the algorithm. The convergence property states that an algorithm must be correct for solving all problems from the same group of problems (same classification). This means that if you are searching for an array under a certain condition, you should not write an algorithm from scratch when searching under another condition! The previous algorithm you wrote should already satisfy all the search conditions. But the above search function does not meet our requirements. Cause: The code being written is not extensible. Now let's ensure that this algorithm meets all search conditions.

First attempt

let array = [45, 56, 103, 91, 405, 324];

function findInArray(arr) {
    let result = [];
    for (let f of arr) {
        if (f > 50) {
            result.push(f);
        }
    }
    return result;
}

Probability of code reuse: 0-10%

In our first attempt, we'll look at passing the number we're looking for as an argument to the function. Although it is easy to change the number, it is not possible to change the condition. Since we can change the number, let's take the percentage of code reuse as 0-10%.

Second attempt.. removing the argument of the conditional operator

let array = [45, 56, 103, 91, 405, 324];

function findInArray(arr, findNum, op) {
    let result = [];
    for (let f of arr) {
        switch (op) {
            case '>': {
                if (f > findNum) {
                    result.push(f);
                }
            }
            break;
            case '<': {
                if (f < findNum) {
                    result.push(f);
                }
            }
            break;
            case '>=': {
                if (f >= findNum) {
                    result.push(f);
                }
            }
            break;
        }
    }
    return result;
}

We can achieve approximately 30-35% reuse of the code by removing the conditional operator as an argument, but this code is also very weak in terms of extensibility, "unreleasable" code. The reason is that if there are binary conditions (for example, finding numbers greater than 100 but less than 300), the function does not meet our requirements. Also, depending on the requirement, we will need to add new conditions. But how do we make sure that the written function can satisfy all the search demands?

It is time for callbacks or "Show me how you do!"

To be able to perform the problem by the mass property, it is necessary to remove the conditional block from the function and write it in the argument. But in the programming language, the if block cannot be passed directly to the function as an argument. To overcome this, it is necessary to extract the if block into a separate function and pass that function as an argument to the main function.

let array = [45, 56, 103, 91, 405, 324];

function findInArray(arr, callback) {
    let result = [];
    for (let f of arr) {
        if (callback(f)) {
            result.push(f);
        }
    }
    return result;
}
//create separate function for every behaviour
function ifLessFrom50(num) {
    return num < 50;
}
//create separate function for every behaviour
function ifGreaterThan50(num) {
    return num > 50;
}
//send the function "ifLessFrom50" to findInArray as an argument
let ls = findInArray(array, ifLessFrom50);
console.log(ls);
//send anonymous function as an argument
let nextResult = findInArray(array, function(x) {
    return x > 50 && x < 200;
});
console.log(nextResult);
//send lambda expression as an argument
//lambda expression is narrow function here.
let result = findInArray(array, x => x > 50 && x < 300);
console.log(result);

Thus, the function receives a callback as an argument, and we can pass any function as an argument to this function.

If we are going to use the conditional code block we are going to write in more than one place, then it is appropriate to separate those blocks into separate functions.

//every function as an argument
function ifLessFrom50(num) {
    return num < 50;
}
//every function as an argument
function ifGreaterThan50(num) {
    return num > 50;
}

If the given function is only needed within this context, then we can use it in anonymous functions or lambda expressions.

 //function accepts anonymous function as an argument
 let nextResult = findInArray(array, function(x) {
     return x > 50 && x < 200;
 });
 console.log(nextResult);
 // function accepts lambda expression as an argument
 let result = findInArray(array, x => x > 50 && x < 300);
 console.log(result);

How do they use callbacks in practice?

Of course, we can't review all practical use cases, but based on our practice, we will try to share with you the cases in which we need callbacks and the implementation options.

Callbacks, as we say, are the process of passing one function as an argument to another function. In many cases, code repetition manifests itself not only as a primitive constant but also as a block of code. To isolate that block of code from its context and respond to the change, it is necessary to pass the block of code to another function in the form of a function.

Case 1

The DOM model works with event-based development.

Callbacks : The definitive guide

-The place of the DOM model in JS is irreplaceable, and in almost every step you will encounter callbacks in the DOM model. Example:

There is a button on the page, and when it is clicked, we want to execute some block of code.

Case 2

When creating dynamic page components in JS.

When programming the payment terminals, we created all the buttons on the page dynamically so that we didn't have to go in and change the HTML file every time a new provider was added. Again, we use callbacks when any event occurs on dynamic elements.

Callbacks : The definitive guide

Which page to open when you click any button in the terminal is read from the configuration and bound to the component as a dynamic event.

Case 3

AJAX requests work based on callbacks.

Callbacks : The definitive guide

Case 4

Jquery uses callbacks throughout. Most jQuery functions accept callbacks.

Callbacks : The definitive guide

Case 5

When we developed a framework in Javascript, the most common feature we encountered in the project was callbacks.

Callbacks : The definitive guide

Case 6

Almost every API in Node.js uses callbacks.

Callbacks : The definitive guide

Case 7

Angular.js supports the end-to-end callback concept and is widely used in practice

Callbacks : The definitive guide

Case 8

A large part of the basic processes in SignalR used for real-time communication are regulated by callbacks.

Callbacks : The definitive guide

Case 9

HTML5 APIs like WebSocket, Server Send Events, etc. use callbacks in their core and configuration.

function WebSocketTest() {
    if ("WebSocket" in window) {
        alert("WebSocket is supported by your Browser!");
        // Let us open a web socket
        var ws = new WebSocket("ws://localhost:9998/echo");
        ws.onopen = function() {
            // Web Socket is connected, send data using send()
            ws.send("Message to send");
            alert("Message is sent...");
        };
        ws.onmessage = function(evt) {
            var received_msg = evt.data;
            alert("Message is received...");
        };
        ws.onclose = function() {
            // websocket is closed.
            alert("Connection is closed...");
        };
    } else {
        // The browser doesn't support WebSocket
        alert("WebSocket NOT supported by your Browser!");
    }
}