DEV Community

Luca Gesmundo
Luca Gesmundo

Posted on

Introducing Hamo - Zero overhead hooks 🎣

Hi 👋

Today I would like to introduce you to a tiny library I just finished.

It's called Hamo (Latin for hook) and will let you hook literally everything, adding zero overhead.

The problem

Hooks are useful, but often they get in the way. You can implement them yourself with event emitters. But, any implementation would require quite a lot of effort and added complexity to your functions.

And more importantly, depending on how much critical is the function that needs to be hooked, a non negligible increase in overhead with a consequent slow down.

What if you could hook your functions, from everywhere in your app, without decreasing performance of a single ounce?

The solution

With Hamo you can do things like the following.

const hamo = require('hamo');

const [sum, onSum] = hamo((a, b) => a + b);

onSum('after', (result, a, b) => console.log(`The sum of ${a} plus ${b} is ${result}`);

sum(1, 3);

// 3
// The sum of 1 plus 2 is 3

Pretty handy right?

You can actually hook functions before / after / oncebefore / onceafter the hooked function.

Well, that wasn't a life changing example, let's some examples from the real world:

Node

In the following snippet we are hooking a write function.

const hamo = require('hamo');
const { writeFile } = require('hamo');
const { promisify } = require('promisify');

// The `true` argument will notify hamo that the passed function is async
const [write, onWrite, offWrite] = hamo(promisify(writeFile), true);

module.exports = {
     write,
     onWrite,
     offWrite,
};

Then, from everywhere in your app, you can attach or detach hooks and be notified when something is written to disk.

Like a notification system for functions.

onWrite('before', path => console.log(`Writing file to ${path}`);

// Writing file to {path} 
// ... somewhere `write` is busy writing something ...

offWrite('before');

// no more notifies 🌺

Browser

Maybe you want to be notified when a React (functional) component renders.

const HelloComp = () => <h1>Hello!</h1>;

const [Hello, onHello] => hamo(HelloComp);

onHello('before', () => console.log('Rendering Hello...'));

onHello('after', () => console.log('Rendered Hello 🎉'));

// then, when mounting..

const App = () => {
     console.log('Mounting App...');

     return <Hello />;
};

// Mounting App...
// Rendering Hello...
// Rendered Hello 🎉

How it works?

Zero overhead

The hooks that are attached to a function are detected. Then, the body of the resulting handler is dynamically generated during runtime. There are no if statements inside it, as only the pieces strictly needed for the currently active hooks are added to the function body.

So, zero hooks, zero overhead.

Running functions after return statements

How is it possible to run functions from a function that has already returned?
This is achieved by scheduling a micro-task in the event-loop in the following way.

const func = () => {
     Promise.resolve().then(() => console.log(42));
     return 'The answer is';
};

func();

// The answer is
// 42

By running code inside an already resolved promise, you are making sure that the event-loop will pick up the tasks and will schedule them for a little later.

Well, that's pretty much it.

You can check out the repo here:
https://github.com/lucagez/hamo

Happy hooks everyone! 🎣

Top comments (0)