DEV Community

Cover image for Returns, callbacks and the whole zoo
Peter Hoffmann
Peter Hoffmann

Posted on • Edited on

Returns, callbacks and the whole zoo

I'm currently co-developing a standardized communication between two entities A and B. To release my mind from all the thoughts about how an why and balancing benefits and drawbacks of different methods I'd like to share them with you. And maybe your 2¢ will help me optimizing our strategy. I'd like to add that my context is browser based JavaScript but some ideas might be generalizable.

➡ calling

If A wants to call B I find the following ways:

  1. Calling a predefined function/method in the scope of A: [B.]act(param)
  2. Using a common communication transport:
    1. by message: transport.postMessage({action: act, parameter: param}) used in inter-frame and main-thread/worker-thread communication but also (miss)usable within one document context (see Appendix A)
    2. by event: transport.dispatchEvent(new actEvent(param)).

The second point might seem overelaborate but is quite useful for decoupling and even necessary if both entities are not in the same context. One advantage of the second way is that A will just continue to work even if B is (temporarily) not available. But on the other hand B needs to actively listen to the specified events (a message is received just like any other event).

⬅ answering

There are more ways for B to react and submit a success state or return data from acting.

  1. directly return a result in case of ➡1: function act(param) { …; return success }
  2. alike ➡1: call a predefined function/method in the scope of B: [A.]doneActing(success)
  3. alike ➡2: use a common transport e.g. transport.dispatchEvent(new doneActingEvent(success)
  4. use a callback contained in param: param.callWhenDone(success)
  5. return a promise, fulfilled or rejected depending on success return new Promise(function (f, r) { (success ? f : r)(successData) })

The first is the standard way for all non-asynchronous contexts and again the second might be necessary in some cases. Asynchronous decoupling is achieved by callbacks resp. promises while promises seem to be the new "right" way to do it.

conclusion?

What are your thoughts, when should one use either one? Is interchangeability shown in Appendix B leading to one way for entity A and another for B? What about a hierarchy between both entities, would your recommendation change depending on weather A or B are more important?

Appendix

A: inter-window communication using postMessage

class B {
  constructor (targetWindow) {
    targetWindow.addEventListener('message', message => console.log(`B is reading: '${message.data}'`))
  }
}

class A {
  constructor (targetWindowOfB) {
    this.targetOfB = targetWindowOfB
  }
  letBAct (message) {
    this.targetOfB.postMessage(message, '*')
  }
}

let entityA = new A(window)
let entityB = new B(window)
entityA.letBAct('Hy, here is A, are you listening?')
Enter fullscreen mode Exit fullscreen mode

B is reading: 'Hy, here is A, are you listening?'

B: transformation

Finally the trivial, most methods are interchangeable (1 is left out as a target). Here interchangeable nm is defined as the answering entity using method n and the receiving entity using method m.

1 ➝ 2:

doneActing(act(param))
Enter fullscreen mode Exit fullscreen mode

1 ➝ 3:

transport.dispatchEvent(new doneActingEvent(act(param)))
Enter fullscreen mode Exit fullscreen mode

1 ➝ 4:

param.callWhenDone(act(param))
Enter fullscreen mode Exit fullscreen mode

1 ➝ 5:

var returnPromise = new Promise(function (f, r) {
  let success = act(param)
  (success ? f : r)(success)
  /* or */
  f(act(param))
})
Enter fullscreen mode Exit fullscreen mode

2 ➝ 3:

function doneActing (success) {
  transport.dispatchEvent(new doneActingEvent(success))
}
Enter fullscreen mode Exit fullscreen mode

2 ➝ 4:

function doneActing(success) {
    param.callWhenDone(success)
}
Enter fullscreen mode Exit fullscreen mode

2 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  function doneActing(success) {
    (success ? f : r)(success)
  }
})
Enter fullscreen mode Exit fullscreen mode

3 ➝ 2:

transport.addEventListener('doneActingEvent', event => doneActing(event.data))
Enter fullscreen mode Exit fullscreen mode

3 ➝ 4:

transport.addEventListener('doneActingEvent', event => param.callWhenDone(event.data))
Enter fullscreen mode Exit fullscreen mode

3 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  transport.addEventListener('doneActingEvent', event => (event.data ? f : r)(event.data))
})
Enter fullscreen mode Exit fullscreen mode

4 ➝ 2:

param.callWhenDone = doneActing
Enter fullscreen mode Exit fullscreen mode

4 ➝ 3:

param.callWhenDone = success => transport.dispatchEvent(new doneActingEvent(success))
Enter fullscreen mode Exit fullscreen mode

4 ➝ 5:

let returnPromise = new Promise(function (f, r) {
  param.callWhenDone = success => (success ? f : r)(success)
})
Enter fullscreen mode Exit fullscreen mode

5 ➝ 2:

promiseResponse.finally(doneActing)
Enter fullscreen mode Exit fullscreen mode

5 ➝ 3:

promiseResponse.finally(param.callWhenDone)
Enter fullscreen mode Exit fullscreen mode

5 ➝ 4:

promiseResponse.finally(success => transport.dispatchEvent(new doneActingEvent(success))
Enter fullscreen mode Exit fullscreen mode

Top comments (0)