Making SetInterval Declarative with React Hooks
overreacted.io
overreacted.io
I now look at more "modern" functional components /w hooks and find it VERY hard to understand the data a component takes and how it uses/responses to interactions.
It feels like react was class based... and now that functional coding is the "in thing" its trying to shoehorn everything into functional components. In the end it really turns me off to functional programming as I see many developers sacrificing readable/understandable components to save a few lines of code.
Luckily React is only a library and it is up to you how do you use it. I got used to working with classes and objects. For bigger and serious project I prefer Ember.js, which is modern and easy to pick up (https://yoember.com), but for some tiny project React is also a good choice with Typescript.
I guess hooks is a good option for devs who prefer functional programming.
`declare function useState<T>(startState: T): [T, (nextState: T) => void]` is pretty simple function signature and most cases Typescript picks up (infers) that T generic parameter for you based on startState. Almost every other Hook function has a similarly simple type signature, despite the complicated "guts" of how React keeps track of hooks internally. (Which also doesn't seem all that complicated, relatively speaking.)
(I still want a way to represent the order of effects / no effects in control flows rules in Typescript's type system, but linters will handle it fine. At this point it's mostly just a personal toy project/curiosity because Typescript feels like it has enough of the basic building blocks in the type system such as `never` to get quite close.)
But these new features aren't passing that test at all. I struggle to grok what hooks do that I was missing before.
It gets me wondering if the React team is in a position to know when React is good enough and needs to be left alone for the most part.
The good news is that I seem to be able to just ignore hooks and remain very productive. I even started ignoring advice on when to use an SFC instead of a class and found myself happier and more productive with better code.
I wrote about this too:
https://medium.com/@dan_abramov/making-sense-of-react-hooks-...
Hope it helps.
As another commenter note, the OP article discusses the pathological case. An API can make a hundred cases simpler but discussions will always focus on the few ones that got harder. The point of this post is to show how to work through this case and why the _end result_ is more flexible and powerful than before.
But the React stuff I found made a context menu of nested components per image. Effectively <image><contextmenu><menuitem/><menuitem/><menuitem/><menuitem/><menuitem/></contextmenu></image>. For 1000 images that's 6 thousand components created, 6000 things added to the virtual dom to be diffed, 11000 considering the content of each menuitem is actually yet another dom node.
There's tons of react libraries like that where they try to shovel every concept into a component.
Ideally, you would only have a single event listener that handles events for all images.
So your <image> code would look something like this:
return (<><img .../> {contextMenuActive ? <contextmenu>...</contextmenu> : null}</>);You can use the various builtin hooks to do stuff that (in class-based React) previously required implementing methods (eg. componentDidUpdate). Using those methods works fine until you want to split out and reuse part of those methods in other components.
You can certainly try class composition: creating little classes that you call in each of the relevant lifecycle methods. However this adds a lot of boilerplate to components which use the composed objects (as is common with class composition). It's also awkward to share the setState API with these composed classes.
The Custom Hooks approach[1] with React Hooks just makes it really easy to use state and lifecycle functionality when extracting common behaviour to can be shared between multiple components.
React has tried to address this composition issue several times in the past, and I think hooks are the nicest yet:
React's old 'Mixins' feature got a lot of the way there, but still didn't compose perfectly. For example, you had to avoid method and state key name collisions between mixins.
Creating a 'Higher Order Component' is another approach, which is less verbose than class composition, but makes your component tree deeper, and can also be annoying to statically type.
The community also came up with 'Render Props' to address much the same issue.
And of course, you can always just continue to use classes if you aren't experiencing any of these pain points.
Hooks just compose quite nicely in cases where none of these other approaches do.
In most other OOP languages besides JS, the obvious answer would be to use traits.
But I got sidetracked with "but howtf do I make typescript understand the resulting signatures" and then got nerd sniped by something else.
Hopefully eventually I'll get back to it.
https://medium.com/@dan_abramov/making-sense-of-react-hooks-...
I hope you find it helpful!
Hooks aren't _quite_ like traits or mixins or anything else. You can apply the same Hook multiple times and pass values between them. That's what makes them powerful.
Look at my last demo.
https://overreacted.io/making-setinterval-declarative-with-r...
I still want traits because I've got very fond of doing class-with-read-only-members type stuff and being able to compose functionality together works really nicely in that regard
(nonetheless, I very much appreciate your efforts on elucidating hooks - I think most of the reason I'm replying this confidently is as combination of your articles and the reference docs :)
Without any evidence to support it. It's one of those phrases that gets repeated without critical thought.
You think of problems in terms of describing how data changes over time rather than how am I going to make this display. The JSX part of the code becomes an afterthought. In the same way you aren't thinking about what DOM mutations React is actually doing behind the scenes, don't worry too much about how it wires together just describe what it does. Counter is a wonderful example. Look how contained and directed the hooks version is compared to jumbled mess the class version is. Picture that class component was anymore complicated, tracing through these lifecycle methods to figure out what is happening under any number of different input changes. Where the hook tells you in its definition when it updates. Each new behavior is just listed after and self-contained. Consider how refactoring and breaking apart components works in both scenarios.
This paradigm is not new. New to React but not at all new to Frontend JS UI libraries. Ironically when presented in MobX, and KnockoutJS(way back in 2009) it was considered simple. Somehow in 2019 with how far we have progressed, it's too difficult?
This contrived example hasn't sold me on hooks at all.
useEffect(state => {
// reading state.count registers a dependency on the count
});
but much though I've been enjoying mobx for model-ish things w/react I wonder if that would end up being too much spooky-action-at-a-distanceIn addition to the paradigm-mixing the article illustrates, I wonder if the actual hook APIs make things more confusing than necessary at some points. For example, in the `useEffect` hook API:
* the first argument is a function that on every render
* the return value of the first argument is a "cleanup" function which runs when the component is unmounted or before the effect is run again
* the third argument is an array of parameters which controls when the effect is run. If passed an empty array, the effect is only run at mount and cleaned up at unmount.
All the above means that the API is very implicit and doesn't self-document its intent all that well. An example, from the article:
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
I'm sure with more use these hook patterns will become more familiar. But it's clearly a lot harder to learn and understand than the original simplicity that helped propel React to popularity.Dan's arguments that hooks provide for easier abstraction and more composability is interesting to me though - if hooks are going to be "worth it" in the long run, I suspect this is why.
I'd almost suggest that a seperate useEffectOnce function would be better, but I'm sure the abstraction was heavily tested and bikeshedded before it was released in this form.
function useEffectOnce(callback) { useEffect(() => { return callback() }, []) }
Personally, React had a great run, but it's time to move on to a more purpose built language/framework.
This example is meant to be a pathological case. The fact that you can build complex interface logic and bundle it into a function that can then be extended and composed is a huge leap forward.
I do think Hooks will take time for some of the best ones to become standard. While I see some early “lodash for hooks” like libararies appearing, I could also see a really nice PWA that let you search gists for popular hooks or similar.
Hooks are a very nice solution to a very hard problem. But unless you’ve done large scale frontend work and especially more complex interactive components it may be hard to see. Although just getting rid of all the decorators/HOCs is already a big win.
I'm interested to hear more please :)
Just means you can have functional components that respond purely to effects from state changes locally or at global level
This also means that components are simply components, and setTimeouts and everything else is moved out to redux thunk actions. Which makes hooks totally unneeded in my case.
This only works in small apps. I work on a medium sized app at work and we started that way too. It becomes a huge, sloppy mess in short order. Putting everything in Redux is akin to a desktop application that uses nothing but global state. Hooks are great because it keeps the state local, but it can be re-used across components if we need to.
If you recall, I said...
> Hooks are great because it keeps the state local, but it can be re-used across components if we need to.
The use case you just defined is exactly why hooks were created.
Legit question because I don't know hooks very well. To my understanding, hooks are just syntactic sugar to use and update local state in a function component. How does that help you share state between components?
That's where the real power is. Combining the 2. The pattern of use is DI even with a global store like Redux, making it not necessarily truly global. What Context does at a base is potentially use React Components as widely accessed stores. The simplest example usually involves abstracting a Parent Component's state/setState into state, and action functions wrapping a Context Provider.
To what degrees the stores should be laid out hierarchically mimicking the render tree is debatable. But often there is clear hierarchical ownership on large portions of the data. And often the question is what needs to be topmost and what belongs only within nested context. I think the opinion is still out on that. As you have suggested you could manage everything topmost.
Hooks make this really interesting in that they expose this idea of simple change management primitives to any Function component. So while it is still the Context API that allows the data to be shared, Hooks allow for re-usable patterns that can be used out of the box or compositionally built upon. So the Component that contains the provider has the ability through Hooks to do a really good impersonation of Redux(useReducer) or even MobX(useState + useMemo + useEffect). Does this replace those libraries? Probably not. But it is interesting how it gives a built in solution to choose to scale these solution with how you see fit. You have these tools at your disposal without much investment.
function useMagicNumber(default) {
const [magicNumber, setMagicNumber] = useState(default)
return [magicNumber, setMagicNumber]
}
And the following two components that use said hook: function ExampleA({}) => {
const [magicNumber, setMagicNumber] = useMagicNumber(1)
console.log(magicNumber) // 1
}
function ExampleB({}) => {
const [magicNumber, setMagicNumber] = useMagicNumber(2)
console.log(magicNumber) // 2
}
ExampleA and ExampleB share the same code that's run in the hook useMagicNumber, but the state reflected is unique to the component.Unless you already used HOCs (in the Recompose sense).
Every single blog post is obsessed with comparing hooks with classes (I guess because it's the only thing they improve upon). What about HOCs? I'm not sure hooks are really that much of a leap forward compared to them.
HOCs only wart is their pollution of props which admittedly hooks solve (by virtue of using local variables), but hooks have their own warts too as evidenced in this article.
Take this example for instance: https://codesandbox.io/embed/26mjowzpr
It measures out the view, reads out media queries, uses the results to construct a grid, uses a hook to turn it into an animatable, and just spits out the view as if nothing happened. All of this in the same component, with small, re-usable utility functions tapping into the host-components lifecycles. Just by looking at it you know what it's doing.
This is how a component should behave from now on, an entity that hooks into the host, instead of a class the host calls into. In my experience this is the first time you can express logic linearly: x > y > z > view, without wraps, inversion of control and DI.
Now imagine doing this with hocs ... even stacking two is already destroying any hope of guessing what's going on.
> However, this is a common source of mistakes if you’re not very familiar with JavaScript closures.
> The problem is that useEffect captures the count from the first render. It is equal to 0. We never re-apply the effect so the closure in setInterval always references the count from the first render, and count + 1 is always 1. Oops!
Perhaps I'm not understanding the second comment fully... This behavior is completely counter to how Javascript closures work in my mental model. I'm trying to figure out how a state hook differs in execution from a normal variable in module scope.
How does it differ from this [1] example.
Your code avoids the closure problem, but closedVariable would be more like a static variable — it'd be shared by every instance of the hook. Probably not what you intended!
I'm having a little trouble explaining the difference well, I think this jsfiddle illustrates the incorrect case the article is talking about representatively: https://jsfiddle.net/7fLnvz5c/
I haven't used it much, but React gives me the feeling that it's trying to come up with hacks to shoehorn imperative UI (which seemingly can't go away) into the "the state is the UI" model. Anything with timers, or animations, or other things that reach across multiple renders seems to be poke through the nice React model because it by definition requires dropping the abstraction of the UI being defined by the state because the change is more "incremental" than transitioning to a new value.
(As an aside, I love the dark mode support on your website!)
React IS a hack to shoehorn declarative UI into an imperative language and DOM system. All further progress in the field is going to be a collection of hacks.
ReasonML, Elm etc are clear ways forward that attempt to start from a clean slate, but you're trying to do declarative UI on JS+DOM, everything is going to be a hack. React is just the nicest currently available hack.
Everything is just JavaScript (well... TypeScript) and the abstraction is solid enough none of this is a concern you have to deal with.
(I enjoyed reading the article - the writing was solid :))
After looking at the intro: https://reactjs.org/docs/hooks-overview.html
My understanding is that each time that you call useState the renderer is keeping track of a state associated with the currently rendered component.
It’s a clever solution, but that implicit context bothers me a little bit. For comparison, Flutter makes the state relationship explicit: https://docs.flutter.io/flutter/widgets/StatefulWidget-class...
The Flutter approach is more verbose, and I don’t know if I like it or not. But the useState looks like a maintenance nightmare. There is no way to type that function usage correctly, and the dependency is not explicit (making refactoring and testing cumbersome).
Any experiences to share?
Even now it's hard to find someone good at idiomatic React, imagine the same situation when it's perhaps not as popular as it once was and internal projects are mature.
Codebases using declarative style are very hard for majority of programmers to grok in my experience, mostly because it's such a big paradigm shift and requires truly abstract thinking, also not generally something you learn at CS programs. It will either all be a big mess or require some senior high salaried people do the maintenance. That will be a good time to do frontend contracting for the enterprises and it's coming soon.
That’s fine if it’s one of one or two major libraries that you use all the time every day. But in practice most of us are using dozens and dozens of tools, tools which change all the time, and we have to learn as we go. So these “big learning up front” APIs are just not very ergonomic for teams to engage.
Unless you’re Rails or React or something where you can claim to be basically a programming language or “standard library” in your own right, something everyone on a team will have already dedicated themselves to studying in depth.
Now, hooks give a lot of powerful ui l magic which lets programmers be super productive from day one, but we have a need for articles like this to describe things. It is not trivial anymore. A bit like rails-vs-sinatra story. React arrived to the magical stage and we already had ember for that. React was nice because there was no important magic.
Ah… who cares? I still gonna use it. It just means I will have to hire more senior devs instead of juniors.
Yes, there are pathological cases like this one. Somebody will put this useInterval on npm and then it won’t be a problem. I’m not convinced it be a big issue for beginners in practice based on my experience showing it to people and seeing them play with it.