Generic “Hooks”
Why Observable + Syntactic Sugar is the Answer™ (maybe 😉)
--
TL;DR: this is a proposal for adding first-class support for reactivity to the JavaScript language. You can see a limited proof-of-concept near the end of this article.
A little over a week ago (10/25) at ReactConf Dan Abramov & Sophie Alpert announced the Hooks proposal. Instead of rehashing things here, I’m going to jump right in. If you’re lacking context, check out those links and Dan’s article.
Perhaps the most impressive thing about hooks has nothing to do with the API itself. The amount of activity around this proposal is astounding.
It’s a bit disheartening to see so much energy being poured into a framework-specific solution to (primarily) managing state. And to see vue, svelte, preact, etc. implementing (or thinking about implementing) similar solutions, again tied to their framework.
Additionally, hooks have some footguns as pointed out by Evan You. As well as a set of restrictions that stem from limitations of the implementation. The React team is aware that these restrictions are likely to cause issues, so they’ve also shipped an eslint plugin to help you not break things. Despite these things, I do believe the React community is going to greatly benefit from the compositional patterns hooks open up.
Side Note: To be fair, hooks are no more composable than the render props pattern — you just get to avoid nesting in the component tree and to write something that looks more like JS. Which is great. But components like <state> and <effect> could easily be created. And imagine if you could have used render props without nesting as suggested by Jamie Kyle.
Before we go any further, let’s get some context on where I’m coming from and rewind a bit. For those that don’t know, I work on a view library called Marko which depends heavily on a compile stage which analyzes the template and produces optimized code for the server and the browser. Marko is essentially its own language that is a mashup of HTML and JavaScript.
For the past year or so I’ve been toying with some pretty drastic changes to Marko with the goal of reducing Marko’s API surface to just language features and a set of core components — essentially dropping the class. I’ve been calling this concept Marko X. I’ve also been driving the rest of the team insane constantly throwing out terrible ideas.
The primary challenge, of course, is how to represent state and lifecycle events. On the state side of things, I’ve looked at a number of different approaches including observables, a reducer-like api (but I didn’t want another api), proxied values (including some crazy ideas to have primitive proxies/pass primitives by reference), and watching the values in a scope.
That last one — watching the values in a scope — is the one that has intrigued me the most. It’s a bit out there, but I’m not the only one thinking this way. And we could do it, because as Rich Harris aptly put it, “WE’RE A COMPILER, MOFOS.”
To illustrate what that might look like in Marko X:
That looks quite beautiful, but the beauty kinda starts to fade when you think about how to extract functionality. Let’s say I want to extract the increment out into a separate function. Because the scope is the source of truth, I need to pass data from the scope to the function and set it back into the scope.
And I can’t just receive data from a source such as geolocation or network status. I’d have to listen for events and set data back into the outer scope. Which is a bit more convoluted than, for example, an equivalent custom hook.
Wouldn’t it be great if there were a better, framework-independent way to represent a value over time? At this point I think I can hear Ben Lesh and the reactive streams community crying, “Observables! We’ve been saying this for years!”
Using observables and reactive streams to drive the view is not new. This is especially true in Angular where Igor Minar and the rest of the team bet heavily on observables for the rewrite of v2+.
“observables are used extensively within Angular, and are recommended for app development as well” — Angular Docs
Back to Marko X, then we’ll get to the point
Looking at how Justin Fagnani implemented promises in lit-html, inspired the idea of allowing to interpolate an observable into a template. As new values come, the view would automatically update.
However, assuming that timeObservable is emitting Date objects, I might want to format it differently than the default toString. And to do that, I would need to operate on the reactive stream.
Now, there’s a number of concepts and APIs a developer would have to understand to parse the above code — and I don’t think that’s ideal. Functional Reactive Programming (FRP) requires thinking about problems in a different way, and I believe there’s a reason why this programming model hasn’t seen mass adoption despite being such a powerful paradigm.
The current generation of UI frameworks makes dealing with state quite approachable. The developer only concerns themselves with the current state. And it would be ideal if we could treat observables as values that happen to change over time rather than bringing FRP into it all.
With a combination of compiler transforms and proxies, we could maaaybe get that working. It would be much easier if we knew statically which values were observables. And it would probably be clearer to a developer reading the code as well. We could introduce a new keyword called observe.
It’s kinda cool. It brings a sort of dataflow programming to the template. But does this then mean that you need to write all your logic inside the Marko template? That you can’t extract it out into a JavaScript file? That’s not great. And so the idea was shelved.
That is, until last week
Reading through the React Hooks announcement, and thinking about how this could be implemented in a framework-agnostic way, such that “hooks” could actually be shared across frameworks, it became clear: Observables are the answer (maybe 😉).
Here’s how the state hook could be implemented as an observable:
Nice! I mean… that component got kinda nested though. Let’s tackle that from the outside in.
First up, observeComponent. This function creates a component that knows how to deal with the observable that is returned from the inner function. However this could be eliminated if React knew how to handle an observable returned from a render function.
Is this a reasonable expectation? Maybe? There’s a Stage 1 Proposal to add Observable to the language. It would be a core feature of JavaScript along side Promises to complete the picture of values that change over time.
Which leads us to the inner function: pipe, map, lambda — there’s still a bunch of FRP going on here. And this would get more complicated as additional state values, etc. were added. In fact, the nesting looks somewhat similar to how things would have to be done with render props.
But what if just like Promises got async/await, Observables had something similar: observable/observe.
Whoa. That looks a lot like hooks. 🔥
Side Note: This would essentially be syntactic sugar that provides the functionality of the combineLatest and map operators. For other operations, you would still need to use a library function, much like with Promises, you still have Promise.all, Promise.race and user-land apis from packages like Bluebird.
I wrote up some rough notes the night of the hooks announcement. You can take a look there for some of my initial thoughts on this. Ignore the Marko X stuff at the end… 🙈
I’d also mention that, again, I’m not the only person thinking about these things. A couple days ago Paul Gray posted an article proposing an alternate design for hooks. The core of the design he’s proposing is similar in many ways to what I’m proposing here, although he’s approaching it purely from a React perspective and his proposed syntactical sugar is different. I’d definitely recommend giving it a read.
Oh, and did I mention he’s got a babel-plugin for his syntax already?
As brilliant as this is, I do think that the observable/observe syntax is more in-line with where JavaScript is at. It feels like a natural counterpart to async/await and closer to the way most people write their JavaScript.
Now, let’s take this full circle and crank up the sweetness 🍭
The observable function opens up a whole new JavaScript world where things are driven by implicit reactive data flows rather than imperative operations.
So let’s look back at the idea of watching the values in scope. The way we’re representing an observable, mutable value is with the state “hook.” But JavaScript already has a way to represent a mutable binding: let.
You could see how we could then de-sugar a let statement in an observable function into creating a new state value and observing it. Anywhere in this scope that variable is assigned to, that would de-sugar into a call to the update function.
Framework Impact
You could see how template-based languages like Svelte and Marko could have their rendering logic implicitly wrapped in an observable function. That means this reactive behavior wouldn’t be SvelteScript or MarkoScript… It’s Just JS.
Side Note: In reality, compilers like this will still be making optimizations and have some differences, but there would be a basis in JavaScript for this type of behavior.
As far as completely replacing hooks with framework-independent observable functions, there are still certain hooks that need access to data provided by the framework to function:
- useContext needs knowledge of the tree in which it is rendered.
- useEffect is probably unnecessary in this new JavaScript world as observables can provide a function to cleanup when they’re no longer needed. But the nuanced timing of when useMutationEffect and useLayoutEffect are called could not be replicated apart from the framework.
These hooks could still provide observables, but when being created they would need knowledge of where React was in the render tree. Depending on the implementation this could bring back some of the limitations of hooks.
Demo (hacky, fragile, and limited — but kinda works)
The above demo is a poorly implemented babel plugin with many limitations that shouldn’t be there. But it’s cool to see it kinda working.
Thanks to Dylan Piercey for getting the plugin started and basically being a walking babel api reference. I apologize for the state this ended up in...
Challenges
The reactive programming wikipedia page has a pretty good overview of the challenges faced by reactive programming languages.
- Do we allow cyclic dependencies? If so, how do we allow breaking the cycle?
- When a value updates, do all expressions in the observable function re-evaluate or only the ones that depended on the value? That is, do we implicitly memoize things?
- If there is implicit memoization, what are the rules around that and how can the developer be explicit?
- When observables depend on other observables, how do we update them without tearing them down and recreating them?
- Many more.
Final Thoughts
There’s definitely a number of challenges associated with introducing a language feature such as this, and at this point I probably have more questions than answers.
That said, the advantages necessitate that we take a serious look at it. JavaScript is already a multi-paradigm language and this takes it one step further.
What do you think? Is this the future or a terrible idea? Leave a comment or shoot me a tweet (@mlrawlings).