Thoughts About Hooks

Andrea Giammarchi
6 min readDec 21, 2020

--

TL;DR If you care about raw performance, hooks are likely the wrong abstraction, but if you care about DX, hooks are simply a wonderful, and fast enough, solution.

This post would like to summarize, on a high level, my “Good, Bad and Ugly” thoughts around this topic.

The Good

Few days ago there was a Hacker News discussion about reinventing the programming mental model, to make it easier for humans to express their intent, and I believe hooks are something close to this idea.

Mostly suitable for UI tasks, easily adaptable for IoT projects, or anywhere there is a runtime that never sleeps, it’s indeed very hard to find other patterns that reason as well with developers intent:

  • hooks seamlessly integrate with any callback and keep each individual state confined (per closure)
  • components work both as standalone and as part of an App, but these don’t need an app to provide 100% of their functionality, neither a framework, for what it matters
  • components can “leak” their state through different returns, accordingly to their inner state, and libraries written well will handle inner changes from their outer components without needing to dispatch ugly and awkward custom events that would require top level orchestration in case two components that depend on some state are placed in different DOM tree branches …

… where likely the latter point is the main reason hooks easily win on the Web, as we can only dispatch events that bubbles up, but we can’t dispatch events that “capture” down, so that each DOM node needs to register itself to a generic top level controller that would notify changes down each component.

Think about a classic shopping app, where the addItem(id) is performed in the main content, through a button, but somewhere on the top <nav>, outside the main content or its branch, there’s a basket that needs to update its items count … well, with hooks this part is covered either via useContext, or simply doing nothing different, as the stack can handle changes without ever touching the event system.

The Bad

While the developer facing experience is nice and clear, every hook implementation inevitably uses more RAM, requires more GC operations, and results into sub-optimal raw performance, because every helper, either custom or native, uses an excessive amount of one-off callbacks, objects, stack operations, and so on.

const comp = hooked(() => {
const [value, update] = useState(0);
setTimeout(update, 1000, value + 1);
});

This tiny auto increment counter example does the following:

  • wrap the callback with another callback that is the hook
  • reset the hook state per each invocation
  • return a new array with the current value, plus a new update function, which is in charge of re-invoking the hook when used

The counter non-hooks-based example would look instead like:

function comp(value) {
setTimeout(comp, 1000, value + 1);
}
comp(0);

Virtually providing the same functionality, our comp is a single callback that does something that doesn’t need to allocate anything new each time is invoked, and the “counter” increments the same way but no array, callback wrap, state reset, closures, or stack, is involved.

Add the hooks based library size to the equation, and here we can see vanilla coding easily beating hooks solutions in all measurable fields: code size, RAM usage, GC operations, and raw performance.

The main question though is: “does it matter”? And in my humble opinion, the answer is a classic: “it depends”!

For instance, when I’ve developed µhooks I’ve made the opinionated decision to make updates always asynchronous, unless the outer hook triggers an update before its nested hooks, but everything state, or effect, related, is basically a Promise.resolve().then(update) away, meaning performance is unnoticeable in the real world, but common benchmark will see hooks always “late to the view update party”, compared to other approaches.

The truth is that no human can really spot the difference though, but if it comes to RAM and heap usage, hooks will always be greedier than most non-hooks based alternatives.

But I guess I’ve stressed this enough already, and if we consider this is actually the only bad part of this pattern, I’d say pick hooks over anything else if your app is not gigantic and your targets are not devices with 128MB of total RAM or less, and forget about the heap until it is the real issue you’re facing (but it’ll be a long ride before you’ll reach that point though).

Side note: I think hooks can work well with Espruino boards too, o Raspberry Pi Zero, without noticeable slowdown.

The Ugly

It took me 3 different iterations to implement hooks in the best possible way I could think about, and what came up while brainstorming each implementation, is that it’s extremely easy to do the wrong thing with hooks:

  • unnecessary invocations at distance through useState instead of useMemo or other better patterns
  • creation of one-off objects aimed right away at the GC when useRef({}), as example is used, together with useState({}) or every other pattern that create callbacks on the fly, objects on the fly, and so on
  • use useLayoutEffect instead of useEffect for anything that should never be synchronous, and with outer references within their callback that likely point at the previous value of anything they refer, easily resulting into “harakiri call-stack loops

In few words, using hooks without digging a bit more about how these work, brings easily the same result as writing JS without knowing anything about it: too many foot-guns that could make debugging of any issue way harder than it should be, also because hooks are a hell of an abstraction that’s everything but fun to “step-in” while debugging any session.

So … now what?

Hooks are also easy to copy and paste, but I think knowing at least the basics around this topic should be mandatory for everyone willing to try.

The official React Hooks documentation is an excellent starting point, and it’s not confined to React usage only, as pretty much every library that offers hooks will work similarly, although you might wonder what other library exists out there, and how close are these libraries, so here a quick summary:

  • uhooks is my latest, greatest, ~0.8K all inclusive library, that brings hooks in both the client, the server, and the IoT world. Its size and performance has no equivalent that I am aware of, and it tries to work as similarly as React does, with the only noticeable difference that useContext needs a reference created via createContext that should ctx.provide(newValue) whenever is needed. See this live example to better understand it.
  • uhooks-fx enables state change propagation to outer components, which is particularly useful to combine all changes at once, delegating the update to the top-most component that is rendering the view.
  • uhooks-dom orchestrate useEffect cleanups to be invoked automatically when nodes, returned by a generic hook, leave the DOM
  • uland which is based on previous libraries and tries to provide a React-like experience

With this array of hooks based utilities, going from ~0.8K to max ~4K combined, I hope there’s already a decent playground to experiment this pattern, which I’m personally liking more and more, despite having been grumpy about performance and GC hell that this pattern causes for long time … but:

  • it’s handy
  • it’s easy
  • it actually works

So give hooks a try, and feel free to ask me more in the (ugly) forum I’ve put in place to discuss all these topics and more 👋

P.S.

I’ve mentioned to get hooks implementation right is not trivial at all, and if you’d like to dig more about it, feel free to check my good old article that tries to explain how hooks work behind the scene, maybe it will help to also avoid over-bloating RAM and GC in your next project 😉

--

--

Andrea Giammarchi
Andrea Giammarchi

Written by Andrea Giammarchi

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.

No responses yet