DEV Community

Cover image for From callbacks to callforwards: a simple approach to reactivity
Dario Mannu
Dario Mannu

Posted on • Edited on

From callbacks to callforwards: a simple approach to reactivity

Back in the middle ages we had callbacks in Node.js and UI interfaces were hardly reactive. Remember that?

Then we had RxJS and you complained it was hard.

Ok, so, how about this? Plain functions, chained together, with each one calling the next when ready.

Image description

This concept is inspired by Express.js, Continuation, RxJS and Observables, except these functions are imperative, non monadic, not observables, with no functional operators to learn...

How does it work?

First you define your pipeline, every step of the way. Do you want to count the clicks on a button? That would be simple, a single-step operation that may look something like this:

const counter = (cf, initial) => {
  let count = 0;
  initial(0);
  return (data: any) =>
    cf(++count);
};

const stream = compose(
  counter
);
Enter fullscreen mode Exit fullscreen mode

Now you have a stream that takes events in (e.g.: button clicks) and will emit a sequence of 1, 2, 3, ... back into the DOM.

How? A proper UI library or framework should take your stream and connect it to the DOM. Rimmel.js may soon support this concept, so you could just do it as follows:

document.body.innerHTML = rml`
  <button onclick="${counter}">click me</button>
  <div>${counter}</div>
`;
Enter fullscreen mode Exit fullscreen mode

A multi-step example?

Let's create a button that emits twice the count of clicks, only when the count is an even number, with 500ms delay.

const asValue = (cf, cb) =>
  (e: Event) =>
    cf(e.target.value)
;

const toNumber = (cf, cb) =>
  (data: string) =>
    cf(Number(data))
;

// even = ~x&1 if you missed it
const isEven = (cf, cb) =>
  (data: number) =>
    ~data&1 && cf(data)
;

const double = (cf, cb) =>
  (data: number) =>
    cf(data*2)
;

const delay = (ms) =>
  (cf, cb) =>
    (data: any) =>
      setTimeout(()=>cf(data), ms)
;


const stream = compose(
  asValue,
  toNumber,
  isEven,
  double,
  delay(500),
);

// then add it to your UI component,
// as you would normally do
document.body.innerHTML = rml`
  <button onclick="${stream}">click me</button>
  <div>${stream}</div>
`;
Enter fullscreen mode Exit fullscreen mode

I have personal doubts about this pattern. RxJS, Observables/Observers, Monads have a solid theoretical foundation, but something like this might also work, or at least we want to figure out, shall we?

Async components? Yeah, sure!

You may easily realise that async functions with any number of await steps would work seamlessly in this scenario.
Let's examine what a step performing a simple API call maight look like:

const callAPI = (url) =>
  cf =>
    async (data) => {
      const response = await fetch(url);
      const json = await response.json();
      cf(json);
    }
;
Enter fullscreen mode Exit fullscreen mode

As oversimplified as it could be, could of course be extended with error handling, authentication and everything...

Unbeatable raw performance ๐Ÿš€ ๐Ÿ’ช โšก

The second reason you might like these callforwards is performance. The compose function will chain the data=>cf(data) calls above, so they would essentially look like this:

// pseudocode
(e: ClickEvent) => {
  cf(data.target.value) ->
  cf(Number(data)) ->
  ~data&1 && cf(data) ->
  cf(data*2) ->
  setTimeout(()=>cf(data), ms) ->
  target.innerHTML = data
}

Enter fullscreen mode Exit fullscreen mode

In fact, either transpilers or JIT compilers may inline these steps in a single function and optimise these steps behind the scenes, so that the above becomes effectively the following, one-step handler:

(e: ClickEvent) => {
  const n = Number(e.target.value);
  if(~n&1)
    setTimeout(()=>target.innerHTML = 2*n, 500);
};

Enter fullscreen mode Exit fullscreen mode

Raw performance isn't everything

The above might be great for certain types of interactions, keeping your bundles as small as vanilla.

What if you're running some heavy animation involving thousands of particles on the page?

Rimmel is introducing a number of optional rendering schedulers to choose from. You can also bring your own, complete with your priority queues and fine-tune every single detail.
More info coming soon. Stay tuned, but that's one of of the most sensible solutions on the horizon.

Should we fear memory leaks?

One short answer may easily be: "always"...

However, if you're doing streams oriented programming and your code is just pure reactive streams, your framework should be able to reliably attach and detach them, drastically reducing the chances of any streams-related leaks. In practice this means turning Angular's "always unsubscribe" mantra into its opposite "never bother unsubscribing", which is one of the ultimate goals here.

Callforwards in action

Want to play? We have a working experiment you can play with: Open in StackBlitz

Do you think this is a very bad idea? Or a very good one? Not sure? Leave a comment below.

Learn More

Top comments (0)