When the React team released Hooks in React 16.7.0-alpha, I was sceptical at first. I expected most of the added features to be unnecessary bloat (I felt similarly about ES async/wait, class syntax, and React async rendering for example). I read through their docs anyway and it was, at least partly, a pleasant surprise. They’re now favoring function components over classes (i.e. functional over object-oriented) and finally presenting a way to react to lifecycle events in functional components. I agree with them that classes are confusing, and using the lifecycle methods forces you to couple unrelated pieces of logic together.

Later that evening, the following thoughts started to brew in my mind:

Hooks might help me in dealing with lazy-loading in function components. I might be able to use them for minimal-boilerplate rendering of reactive Properties, so that a React component would be automatically (reactively!) re-rendered when a value changes. You could use them for local/modular state containers, reducing the boilerplate to create state atom on mount and reusing it on subsequent renders.

This was all going on in my head as I was trying to fall to sleep. Obviously, I couldn’t. Instead, I had to do some digging. Here is what I found out.

Spoiler alert: you can indeed do all of the above with Hooks. In fact, I wrote a working application you can tinker with if you read on!

The background

I feel obliged to give some background to explain why this matters to me in the first place. Feel free to skip this section if you’re just here for the free code samples.

As an FRP fanboy and the author of the Bacon.js library, I always seem to be searching for ways to utilize FRP in UI (or any other kind of) development wherever I go. Handling state in React applications has been one of my favorite problems for a few years already, as some of my colleagues know. For instance, I organized a workshop on different state handling methods in React where we covered these approaches (linked with a running todo application implementation):

Redux where the entire application state is in a Redux store and it’s modified by Reducers that process events to create a new modified application state

Bacon.js Megablob where the application state is similarly composed into a single reactive Property, the value of which is then rendered using React components

Calmm.js where the application state is in one or more atoms (based on Kefir.js / Bacon.js) and it’s decomposed using functional lenses

Redux seems to be the de facto way of handling React application state nowadays, but I find it forces you to spill a lot of boilerplate and files and decouple events and consequences in such a way that it’s hard to figure out what’s going on. Furthermore, the applications are typically organized so that you put all chairs in one room and tables into another room (replace the furniture in my example with “containers”, “components” and “reducers” if you like).

The Bacon Megablob gives you some of the benefits of the Redux architecture, namely those that are derived from the application state being in a single place as an immutable object. You can easily externalize state to implement things like going back in history or transporting state from the server-side to the front when doing server-side rendering. On top of this, the Megablob gives you direct traceability of code flow: the megastate is defined in a declarative way so you don’t have to resort to string searches to trace what happens when. The Bacon.js library as your state handler also supports asynchronous workflows smoothly so you don’t need any extra “middleware” for that.

But neither of these approaches helps you with modularizing/localizing your application state. Storing every piece of state in a global blob starts getting cumbersome at some point. This is why we often resort to using setState for a piece of local state that’s inconvenient to make global. Now consider a component that needs to lazy-load, i.e. fetch some stuff on the background, based on its props. When props change, you want to discard the current result and re-fetch based on updated props. This is where the Calmm.js approach shines. You typically use properties or lensed atoms instead of static values as component props, and can easily react (pun not intended) to changes and handle asynchronous workflows gracefully.

Rendering observables by using Hooks

The next morning, I had to try using Hooks to see if they could deliver the benefits I dreamt of the night before. I started with attempting to render Observables using Hooks. In my example code, I use Bacon.js Properties in particular, but the same method is easily transferable to RxJs, Kefir or any other implementation of observables.

It boiled down to a custom Hook, named in compliance with the Hook naming convention:

export function useObservables(...observables) { // Combine values from all observables into one array const observable = B.combineAsArray(observables); // Get current value of the Observable for synchronous rendering let valueArray = getCurrentValue(observable); // Hack: use write-only state for triggering updates const [, setV] = useState(valueArray); // Listen to changes in the Observable useEffect( // The returned unsub function (returned by forEach below) can be used by react to clean up. Nice! () => observable.changes().forEach(setV), // Below line causes resubscription to occur only if the given array of observables changes observables ); // Sanity check: we need the current value to be able to render if (valueArray === valueMissing) { throw new Error("Current value missing. Cannot render"); } return valueArray; }

That’s it! Just add Bacon.js and you’re good to go. Well, there’s a couple of simple helper methods, but you get the idea.

It turned out that by using the useEffect Hook it’s easy to subscribe to changes in the Observable after rendering. The Hook automatically cleans up the subscriber when the component is unmounted and before the next render. By passing the Observables array as the second parameter to useEffect I ensure that resubscription to the observables occurs only when the list of observables has changed. This is important for some stateful observables that would lose their state if we resubscribed on every rendering.

In addition to useEffect, I use the useState Hook to trigger a repaint when a change is detected by the subscriber set in the useEffect Hook.

Now you can, for instance, write a simplistic component using the useObservables Hook like this:

const Text = ({ text }) => { const [t] = useObservables(text); return <React.Fragment>{t}</React.Fragment>; };

This simple component accepts an Observable and renders its current value. When the observable’s value changes, it’s re-rendered automatically. Nice, eh? React Hooks made it relatively easy to implement inside of a function component, without any wrappers or other boilerplate.

Making state modular

Earlier I mentioned modular state. By this, I mean that I don’t like having to use different ways of implementing state depending on whether a piece of state is local or global. The code should be such that I can choose the most appropriate level for storing each piece of the application state. Furthermore, I want to be able to change those decisions later without having to do a major rewrite (like when changing local setState code to use a Redux store instead).

I find Atom a very appropriate abstraction for a piece of application state. It’s a “box” that holds a value and you can set and get the value. In addition, it should support the Observable interface, allowing you to react to changes in the atom’s value. An example implementation is Bacon.Atom which I’ll be using in my example code.

So now, using the useObservables hook and an Atom, I can define an Input component that reflects the text stored in the given Atom and sets its value on user input.

const Input = ({ value }) => { const [v] = useObservables(value); return ( <input type="text" value={v} onChange={e => value.set(e.target.value)} /> ); };

This component is agnostic to where the application state is stored, as long is it’s given an input atom. However, I can also modify the <Input> component so that it can work with the provided external atom as well as standalone by creating its own state atom if not provided:

const Input = ({ value = useAtom("") }) => { const [v] = useObservables(value); return ( <input type="text" value={v} onChange={e => value.set(e.target.value)} /> ); };

This uses a little custom Hook:

export function useAtom(initValue) { const [atom] = useState(Atom(initValue)); return atom; }

This simple Hook creates a new Atom on its first rendering and uses the previously stored atom from the application state on subsequent renderings. With this Hook, you can write components that can work both with external state and with a piece of local state in case an external state atom is not used. Much modular!

Just add lenses

This section is not directly related to Hooks, so feel free to skip it (but don’t actually skip this, as you should definitely check out lenses). Atoms without lenses won’t get you far. An atom-based state gets 100% cooler when combined with lenses, especially the great partial.lenses library.

Here’s have a quick look at what lenses can do via a simple example.

Let’s say we’re using atoms for state and are writing a Contact component for a contact list application, allowing the editing of the name, email and phone fields of a contact. We have:

an atom called Contact, which allows us to view the current contact object using contact.get(), as well as reacting to changes in it using contact.forEach(fn) and also replacing it with a new contact object using contact.set(newContact)

the <Input> component that can be used for editing the value of an atom of text

All fine, except our atom contains an object like { name, email, phone} while our input component expects an atom containing a single string. We could create a new atom for each field, then subscribe to changes in each one of them and .set the value of the contact atom on field changes. But we might also need to take care that changes flow the other way around.

The thing that saves our bacon here is lenses. The atoms (in bacon.atom) have a .view method that allows you to create a lensed atom that reflects the state of some part/slice of the original atom. In this case, we can create lensed atoms for each of the contact object’s fields simply by contact.view(“name”). These atoms behave exactly like any other atoms, except the data is actually stored in the original atom and changes are reflected both ways. So we can do something like this:

<div className="field"> <label>Name</label> <Input value={ contact.view("name") } /> </div>

To recap, with atoms you can represent application state using a single abstraction – and with lenses you can easily decompose it, showing the relevant slice of the full state to each component. You also have the flexibility to have state in a global atom as well as local atoms where convenient.

Conclusion

Right now, Hooks seem quite promising and remove my last reason to use class components in React.

The one thing that slightly bothers me with Hooks is that they rely on execution order. Take useState, for example. If the useState calls end up in a different order, the application state will most likely be badly messed up. React documentation clearly points out that you should only use Hooks on the “top-level” to ensure proper execution order (and execution on every render).

So remember: you should treat Hooks like inheritance and not like functions. Then you should be safe.

Disclaimer: This is all just a quick experiment, my conclusions may be wrong. Please shoot this down as soon as possible!

All the code, a working contact list application, and the custom Hooks can be found in this CodeSandbox application.

I also rewrote the todo application mentioned in the background section using my new Hooks. And it turned out pretty nice.

Hats off!

A huge thanks to Vesa Karvonen for support in writing this post and particularly the partial.lenses library. Thanks to Reko Tiira for your excellent Calmm.js workshop at Reaktor and the contact list application that I used as the base for my Hooks experiments.