Vanilla Hooks (outside React)

Andrea Giammarchi
Jan 4 · 13 min read
Image for post
Image for post
Photo by Vishal Jadhav on Unsplash

Hooks are a pattern, not something usable with React library only, and this post would like to explain, and walk through, some interesting possibility.

What are hooks?

Hooks are nothing more, and nothing less, than a wrap around a generic callback. The callback itself is not a hook, unless it’s being handled by a hook-helper, in this case provided by µhooks library, which is the tiniest, and fastest, I know out there, and “it just works” ™.

As example, this is a generic callback that uses internally some hook-helper, but it won’t ever work as expected, unless it’s wrapped by a hook-library helper.

It is important to remember that, unless used as helper itself, callbacks can’t benefit from hooks helpers, because these callbacks need to be orchestrated.

Reading the console logs in this CodePen example, would show an incremental counter that goes from 0 to N, which is what we’d expect.

How does it work?

The short version of the story, is that each time the update function executes, provided by the useState helper, the hooked callback gets invoked again, and the useState helper will know, at runtime, what’s the last updated value to return, ignoring the initial value provided the first time such hook got executed.

An extremely minimalist hook implementation and explanation can be found in this gist too.

The long version of the story is described in this older post of mine, but it’s not necessary to understand all the internals, while it’s necessary to remember that each hook needs to be handled a part, so that to start logging from zero in parallel, as example, we cannot invoke incrementalState(0) again, ’cause we need to create a new hook through the same helper.

The math is simple: one hook is equal to one, and one only, callback wrap.

We can manually invoke a hook as many times as we like, but initialized states will always reflect their current value, so that calling incrementalState(123) won’t ever log, reset, or start, the current counting or its value, and it’ll just create a new setTimeout, so that now everything is logged twice each second.

Feel free to play with the previous CodePen to test it yourself.

Hooks & DOM Elements

I’m pretty sure showcasing hooks with a silly counter gets easily boring, but that’s already the “ABC” of how hooks works, and we can explore some other helper to create standalone DOM Elements, in this case, useRef:

The useRef is a good helper to create anything we need once. Its initial value would be stored to an always-same object reference, and as current property.

Since this is a helper, and not a hook itself, it’s not necessary to wrap it as hooked function, as it will be used within hooked functions instead:

The live demo this time showcases two counters (I know, still boring… bear with me), and the important thing to understand, is that once an element has been created, it will always be exactly the same element:

We can update its prefix content via simple('just simple') and it’ll still be the exact same element.

Reactive State

As we’re already introducing components definition through hooks, and since counting is not even close to be a real-world use case, we can already find a way to avoid writing useState all over the place, for each single state to handle, and deal instead with an object that would provide state details and it’s able to update the current state with ease:

With above helper in place, the Button component factory can be simplified in a more readable way, that would also scale with other properties:

Check this live demo to play around this pattern.

Handling Events

Similarly, as we could easily have more states to handle, we could as well have more events to deal with, and here the little gotcha:

  • DOM Level 0 events are both easy to set, as element.onclick = thingy, but also easy to mess with, because there can be only one Level 0 event set per element, so that our component could easily either leak its listeners, or never react due some other script event override
  • tracking previous listeners, so that these get removed and replaced per each state update, is tedious and error prone

To help solving these pain points, there’s the great handleEvent standard, that would allow us to never care about tracking previous listeners, ’cause if the handler is always the same, nothing happens twice or more:

With above helper, the Button factory now would look like this:

Feel free to play around the related live demo.

Combining Helpers

If two helpers are somehow frequently used, and there is a way to disambiguate their intents, it might be a good solution to put these helpers together, avoiding repeating the same code each time.

With above helper, we can now define both handlers and reactive properties at once, making our Button factory look like:

Once again, check the live demo to see how this works.

What have we learned so far

The main take-away, up to this paragraph, is that hooks are just primitives able to compose really well together, so that creating ad-hoc helpers for our cases should be straight forward, as long as we pick “the right hook for the job”.

We’ve only explored useState and useRef so far, and that’s scratching the surface of what’s possible to do with hooks, but I hope it’s clear we can already do a lot with just these two primitives around.

Now take a break, have a walk, or play again with what we’ve done so far, so that once you’re back, we can talk about more complex examples 😉

A “Todo” App

I know this won’t be overexciting for many, but the point of X-App is to relate inner components with the whole App itself, as each component update could require an App update too, and having nested hooks is also something to talk about, as there are various solutions to consider to compose this pattern.

For example, each button described so far, is a standalone component, also able to implicitly dispatch a click event to any outer component, but knowing how to deal with delegated events, within hooks, and somehow shared data, might be worth this extra example.

In a nutshell…

What we are going to create is a very simple structure that allows users to type some item, add it via Enter, being able to flag each item as done, or not.

The very first version of this app is live in CodePen, so let’s see what it does.

Image for post
Image for post

The structure

We won’t focus too much on this structure correctness (it’s not), rather on its functionality.

Like any other dynamic application, we’ll be dealing with some data, in this case representing a list of tasks, or items, where each task carries most relevant information: value and done.

The Task/Item Hook

Hopefully self explanatory, if we understood previous part of this article, the Item component is a standalone <li> element that would change its className whenever its state changes, simply updating its reactive checked property, after also updating its reference value.

Differently from previous examples

The useElement presented before was setting elements properties only once, but here the helper should be capable of updating these, in case of changes, each time.

The Todo App

At this point all we need is a container able to render these items and create new items too.

To recap:

  • the App is a hook that handles a list of tasks/items via an input field, and it’s capable of adding new tasks/items when the Enter key happens
  • the App updates its content each time a new task is created, and it relates each item to a unique data-point used as weak key.

The ugly parts…

  • the item leaks its state through data mutation, and such change cannot be detected outside its closure, unless each item is reactive too, but that’s rarely the case (thinking about JSON requests, etc). In case we’d like to show the total amount of tasks, and the amount of completed tasks, changing items state without updating the whole view won’t easily scale.
  • compared to React and its JSX special syntax, it’s easy to feel like components creation and initialization is pretty verbose and error prone
  • the WeakMap is created each time for no reason, which is easy to fix, using the .current trick via useRef, but also more verbose for very little gain

there are probably other ugly parts, but these 3 make me already want to rewrite, or refactor, this Todo project… but how?

Introducing µland

The µland module is a µhooks based library that tries to improve the vanilla hooks states in the following way:

  • it’s declarative, thanks to µhtml exposed via its core
  • it provides helpers to coordinate containers/components out of the box
  • it requires less boilerplate, helpers, around

Our new version of the App would indeed be reduced to 45 LOC, where the Item component would now look like this:

so that no previous helper is needed anymore, as defining elements, their attributes, events, or properties, using the .propName convention, is integrated in the template literal based engine.

Not only the Item component is simpler, the Todo component is also now shorter and easier to read:

The Todo’s update(count + 1) would re-render the list of items, after prepending the last entry, while the Item’s update() would propagate through the top most rendered element, so that now completed tasks would also be reflected in the main container as data-info.

Define the element where to render our new application, and see it live.

Keyed vs Non Keyed

If we look closer to the current demo, there is something to consider:

The template literal engine is smart enough to update each time all properties, but without a reference we’re working in a key-less mode.

Inspecting the current DOM, we’ll see that changes might be applied to multiple items, but if we’d like to be sure that a single item represent a specific component, we still need to use a reference, in both the Todo app, and within each Item.

In order to do so, we can use another hook helper: useMemo

Such helper executes the callback only when its guard changes, creating in this specific case only one WeakReference, as opposite of creating it per each update as it was before.

The items.map dance can now use such reference, to create new components only when it’s needed, and re-order (DOM diffing) others instead of updating each time the whole list.

The other change, at this point, is to relate such item to the piece of DOM it represents, in a unique way:

The html.for(ref[, id]) is the way to go, and the id in this case helps us reuse the same item around the page, without also moving the same node.

Feel free to play around with the latest version of this app.

Still missing…

Our current App is still mutating items from the inside, and I can already imagine all “immutable-data” fans swearing…

It’s worth also considering that the empty useState, invoked just to propagate the state change, is more a hack than a solution.

What we could do instead, is pass along the application context, which enables any component to trigger an update that every other using the same context will be notified about.

While in React a context is usually represented as yet another JSX node, in uhooks, hence in uland too, there is an utility to create one:

The useContext(context) will return whatever value the context carries, but the context itself also provides a way to update its value, automatically propagating around the application, resulting in a global update:

This last example is also live, and the most important things to notice here:

  • there’s no need to use html.for(...) because each item change becomes a new reference so we’d just bother a WeakMap for not much gain
  • the intent is cleaner than before, but nodes are now key-less again

Regarding the last point, not only it shouldn’t really matter unless keyed results are absolutely mandatory, but if we look closer we could eventually associate each component to its own value, and use such value as unique key, as the logic doesn’t allow us to add twice the same task.

A simple Map in this case would work better, and the only extra thing to remember, in case the App is extended to also remove already done items, is that keys should also be removed from this Map, but I’ll leave this part as an exercise for the reader.

Conclusions

I hope you enjoyed this step-by-step Todo App creation, using both vanilla DOM and tiny helpers born to solve these tasks with ease, so that it should be clear by now that using hooks is not necessarily something confined within the React ecosystem, but something enjoyable via any other standard too.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store