Home > Software design >  Add an extra parameter to a callback function in Javascript
Add an extra parameter to a callback function in Javascript

Time:06-30

Hello Stackoverflow users,

Many peoples like me searched for how to pass extra arguments to a callback function. The questions have similar titles but actually they have different challenges and many ways to solve. Plus, it is always a pleasure to share practices to be more experienced.

Recently, I faced a pretty simple challenge in my node js project. One of the APIs I communicate with has an SDK that works synchronically. And I used to pass callback functions every time (which is annoying when you have requests depending on each other and some data needs to transfer within the app layers).

Imagine a plan payment flow that goes like this, a client sends a request to the server including the selected plan and his ID. When the server API layer receives the request data, it passes it to a third-party service function ( .create(...) ). The third-party service function receives a callback with 2 parameters function(err, plan_document). And then, the callback is supposed to apply the selected plan logic on the client by the ID in the request.

** We need to pass the client's and the plan's data to the callback function to apply the logic. The third-party service provides to the callback a plan_document parameter and we still need to somehow pass the client id from the API layer to the service.

The code will look like this.

const create_plan_agreement = (req, res) => {
    // some code
    var client_id = req.auth.client_id;
    third_party.plan_agreement.create({}, update_plan_agreement);
};
const update_plan_agreement = (err, plan_document, client_id) => {
    /* 
        The third-party `third_party.plan_agreement.create` function passes the first 
        two parameters and somehow we need to add the client_id 
    */
    console.log('client plan activated');
    active_client_plan(plan_document, client_id);
};

------------------ EDIT ------------------

I wonder what if the flow was longer and I need the client id farther than the update function like this.

const create_plan_agreement = (req, res) => {
    // some code
    var client_id = req.auth.client_id;
    third_party.plan_agreement.create({}, update_plan_agreement);
};
const update_plan_agreement = (err, plan_document) => {
    console.log('plan activated, send notification to the client');
    third_party.plan_agreement.update(plan_document, send_agreement_notification);
};
const send_agreement_notification = (err, plan_document) => {
    console.log('client plan activated');
    active_client_plan(plan_document, this.client_id);
};

What should I do in this case? Should I keep repeating the.bind({'client_id': client_id}) function until the last step in the flow?

CodePudding user response:

If you want to support older people, you can easily bind using a containing callback, like this:

const create_plan_agreement = (req, res) => {
  // some code
  var client_id = req.auth.client_id;
  third_party.plan_agreement.create({}, function(params, from, create) {
    update_plan_agreement(params, from, create, client_id)
  });
};

const update_plan_agreement = (err, plan_document, client_id) => {
  /* 
      The third-party `third_party.plan_agreement.create` function passes the first 
      two parameters and somehow we need to add the client_id 
  */
  console.log('client plan activated');
  active_client_plan(plan_document, client_id);
};

CodePudding user response:

The traditional way is to use a closure. Define the functions inside the parent's scope so that they can access the client_id as an enclosed variable (kind of like global variables):

const create_plan_agreement = (req, res) => {
    // some code
    var client_id = req.auth.client_id;


    const update_plan_agreement = (err, plan_document) => {
        console.log('plan activated, send notification to the client');
        third_party.plan_agreement.update(plan_document, send_agreement_notification);
    };

    const send_agreement_notification = (err, plan_document) => {
        console.log('client plan activated');

        // Note: this function can access client_id
        // because it is in scope
        active_client_plan(plan_document, client_id);
    };

    third_party.plan_agreement.create({}, update_plan_agreement);
};

Closures are to scopes what objects are to classes. A closure is an instance of a scope. So unlike regular global variable, each call to create_plan_agreement() will create its own closure with its own copy of client_id.

With modern javascript it is often easier to handle this with Promises. Convert legacy functions to return a Promise and then you can use async/await:

const create_plan_agreement = async (req, res) => {
    // some code
    var client_id = req.auth.client_id;
    try {
        var plan_document = await plan_agreement_create({});
        var updated_plan_document = await update_plan_agreement(plan_document);
        send_agreement_notification(updated_plan_document, client_id);
    }
    catch (err) {
        // handle errors here.
    }
};

const plan_agreement_create = (arg) {
    return new Promise ((ok, fail) => {
        third_party.plan_agreement.create({}, (err, result) => {
            if (err) {
                return fail(err);
            }
            ok(result);
        });
    })
}

const update_plan_agreement = (plan_document) => {
    return new Promise ((ok, fail) => {
        third_party.plan_agreement.update(plan_document, (err, result) => {
            if (err) return fail(err);
            ok(result);
        });
    });
};

const send_agreement_notification = (plan_document, client_id) => {
    active_client_plan(plan_document, client_id);
};

Or even without async/await Promises still make callbacks easier to use:

const create_plan_agreement = async (req, res) => {
    // some code
    var client_id = req.auth.client_id;

    plan_agreement_create({})
        .then(doc => update_plan_agreement(doc));
        .then(doc => {
            send_agreement_notification(doc, client_id)
        })
        .catch(err => {
            // handle errors here.
        });
};

CodePudding user response:

We need to bind the callback function to an object so it can access the object attributes.

Bind the function to an object that includes the data you need to transfer this way:

third_party.plan_agreement.create(request_data, update_plan_agreement.bind({'client_id': client_id})); 

Remove the client_id parameter and read it from this.client_id that refers to the object that the bind function generated:

const update_plan_agreement = (err, plan_document) => {
    /* this. refers to the object we passed to .bind */
    this.client_id && active_client_plan(plan_document, this.client_id);
};

To make it simple, I will describe in steps how the bind function works.

  • it will generate an object like the object you passed.
  • add your function as a method to the object and run it from the object.

Hopefully, it helps someone.

  • Related