Thinking in React Hooks
wattenberger.com
wattenberger.com
My complaints about hooks are that with hooks we lose useful tools in our react toolkit: class methods, separate componentDidMount/componentDidUpdate lifecycle events, shouldComponentUpdate, PureComponent, etc.
And my biggest complaint is that we're losing simplicity. React spread like wildfire when it first came out because it was so beautifully simple: a class, with something called `props`, and this.state and this.setState(). That's it. You can build SO MUCH with just that.
To me, with hooks, React's simplicity is being replaced by cloudiness. There's so much "magic" now. The order of hooks in the code is the most important thing for a hook component to work right. New linter rules had to be built to remind people where hooks need to be written.
I do appreciate the work that's been put into hooks. And like any other open source project, I'm incredibly grateful to the authors and maintainers. I guess I just wonder if anyone shares the same sentiments as mine, because it seems like the only thing I read about this topic is that classes suck, class components are full of pitfalls, etc.
But as you can see by the blog post, there are tradeoffs between doing something the way people are familiar with, versus a different way that's less familiar, but has different capabilities that might fit the problems better.
Classes map to certain use cases very well; for instance, there's no Hook for "componentDidCatch" or any other error handling.
That being said, you are not losing anything. The tools are simply different (although the old tools are still there, too!).
As the blog post goes into, you can still implement the same _behavior_ as you did with componentDidMount/componentDidUpdate, but with Hooks you can formalize the different patterns you would find when using those methods into a custom hook that you can reuse. This wasn't possible before.
shouldComponentUpdate can be easily replaced with React.memo, a higher-order component that takes a function that compares the previous props to the new props and returns whether they are the same. Wrapping it in React.memo without a custom comparator is the same as PureComponent.
Guilty as charged. I needed to hear that :)
I mean, as long as hooks are strictly an addition, it's easy to argue greater simplicity without. That's kind of trivial though, so I would ask how thematically related they are.
If they both clearly stem from the same set of underlying principles and motivations, I think it's fair to say the addition is not substantially a complication. But is that the case for hooks? (I don't know—haven't looked into them yet :)
But maybe the confusion is whether the additional complexity we're talking about is in React itself, or in the systems being built with react. In that case, old React may be simpler, but the code it produced was more complicated; or React with hooks may be more complicated, but the code you write with it is now simpler.
Which I (as a proponent of hooks) think is fucking batshit insane, and clearly shows the disconnect of the React devs from their user base.
Teaching beginners how to code, and how to use React, is a large part of my job. And I can say with absolute conviction that the React team is wrong about this. The 'hooks' whiteboarding session has gotten me far more blank stares than the 'classes and lifecycles' one ever has.
Having said that, as a senior dev with a ton of React projects under my belt, I definitely prefer the hooks paradigm after using it in a few projects. It makes for cleaner code, and certain concepts are much easier to reason about with the hooks model. Just don't ever expect it to give you a cleaner on-boarding process. That's not at all where the benefit lies, despite what the docs tell you.
I'd be interested to see how someone who was a beginner but learnt via FP would grasp each approach. I've never met one in the wild though, it's not like bootcamps start with Haskell.
I also never think about “updates”. I do think about rendering as something that happens continuously all the time outside my control.
Hooks, on the other hand, have to be learned as an additional and separate concept. In fact, each hook has to be learned. You won't have a clue what "useState" and "useEffect" do just by having a knowledge of React fundamentals. If using hooks allowed developers to avoid knowing the React component lifecycle, an argument could be made that they provide a simpler alternative, but I don't think anybody is going to get very far without being forced to learn about the component lifecycle. If someone were to say, "The React lifecycle is hard to understand, so use hooks instead," that would be a false promise. Hooks are something extra to learn, not an alternative to understanding the lifecycle. To the extent that a programmer can avoid understanding the lifecycle by using hooks instead, they are just putting off that learning curve until they have to traverse it later, probably under the pressure of debugging some nasty issue.
Hooks seem to win from another perspective, though, which is that by accepting the cost of adding this new concept to your mental model, you can write simpler-looking code that is easier to keep error-free. To me, that's the trade-off. Hooks are an extra complexity to understand, and each new hook can encapsulate arbitrary complexity and behavior that isn't obvious from reading code that uses them, but the code that uses them can be more concise and perhaps easier to read and keep correct.
Let me preface this by saying that I think it's not necessary to conflate the "lifecycle methods" with the component lifecycle in itself, and I disagree with the premise that the methods are a fundamental concept of React. Of course, this is how people were historically taught, but it is entirely possible to conceptualise and understand the lifecycle of a React component without even knowing about lifecycle methods, just in terms of how useEffect works, for instance, and this is how a lot of people using hooks today think. It is entirely possible to understand how re-renderings are triggered and stuff like that without even knowing about class components and lifecycle methods.
So, hooks might seem like an additional concept to people used to class components and lifecycle methods, but that's just because of familiarity. For people learning hooks first, the opposite is true just as well.
IMO this paradigm shift in thinking is also essential for people used to class components that want to be more productive in codebases with mostly hooks. I haven't thought about componentDidUpdate and friends in months.
I would disagree with that premise. From my own experience and from having mentored developers on class components (so like ~7 data points), I'd say it's far easier to reason about state flow with hooks than lifecycle methods. That's an important point, because I see far more bugs with incorrect or incomplete state flow than I see with component lifecycles.
Hooks are simply easier to reason about. With hooks, you read the code top to bottom. Then, something changes either with the data or in response to user input, and then you read the code top to bottom again with that new state.
The only learning curve there is to understanding the basic premise of hooks is that "if this value is different from the last time you read the code top-to-bottom, then re-read/execute this code block"
> If using hooks allowed developers to avoid knowing the React component lifecycle, an argument could be made that they provide a simpler alternative, but I don't think anybody is going to get very far without being forced to learn about the component lifecycle. If someone were to say, "The React lifecycle is hard to understand, so use hooks instead," that would be a false promise.
That is exactly my premise, and I don't think it's a false one. Since moving to hooks, I can't remember the last time I had to care about the React lifecycle. Again, the mental model is "Read the code top to bottom; something changes; read the code from top to bottom" ad infinitum.
Yes there are situations where class components provide more intimate control (componentWillUnmount comes to mind since I'm not aware of a hook for that), but are we really optimizing the majority of our code for exceptional cases?
Another data point: I haven't seen functional components turn into a complete mess from inattentive developers in the same way I've seen class components turn into indecipherable setState tangled messes. The only time I've experienced the pressure of sifting through component lifecycles under the pressure of debugging a time-sensitive production isssue is because I was using component lifecycles in the first place. It's a moot point.
The return value of invoking React.useEffect’s first parameter is a cleanup function which serves this purpose.
Everyone in this thread is ragging on core concepts like useMemo or useEffect. Once you know these core functions, you’ve learned all the magic. Then you can write spells of your own.
>That being said, you are not losing anything. The tools are simply different (although the old tools are still there, too!).
Well... no, you clearly are losing something. You might not use it frequently but it's a pain in the ass when you do need it.
I'd argue it's less important for an API people use every day than it is for an API used once a month. Things should be designed for people with more than one week of experience with it, even if it makes adoption slightly tougher!
We used to fetch data with HOCs. That never worked well with typescript and was a big problem when you wanted to use more than one (ie, fetching multiple data sources). With hooks we just add separate hooks for each data source we fetch... and the hooks themselves are much simpler than HOCs.
Like the article mentions, hooks move up a layer of abstraction. I'm purging my brain of all the nitty gritty detail of React component lifecycles, because all I ever really cared about was getting data to update at the right time. And now that's the level I can operate at.
Yeah, it doesn't look like standard object oriented code that we're used to. Maybe they could have chosen a different syntax, dunno. But after you get used to hooks, the grass definitely is greener.
I specifically talked about these differences in my post "Thoughts on React Hooks, Redux, and Separation of Concerns" [0] and my ReactBoston 2019 talk on "Hooks, HOCs, and Tradeoffs" [1].
For myself personally, I've completely switched to writing function components as a standard practice, but we've also got a lot of devs who are newer to React and still writing classes. I don't feel a need to force rewriting all of those to function components, although I will probably find some time to let folks know that function components + hooks are a thing.
[0] https://blog.isquaredsoftware.com/2019/07/blogged-answers-th...
[1] https://blog.isquaredsoftware.com/2019/09/presentation-hooks...
edit: i guess i'm just supposed to upvote and move on but i'm enthusiastic about this topic :)
The article doesn't show pathological cases using classes, but with hooks it becomes possible to extract complex behaviors involving different lifecycles much more cleanly.
I recently migrated a large React project to use hooks and a lot of repetitive code with lots of repeated patterns (especially things related to data access) is now much more cleaner. Now I can just have something like `useFetch` instead of ten lines of code that I needed just because I was using lifecycle methods.
And the fact that the order of hooks matter is what makes them so easy to abstract. Having them store their own "id" would remove the need for that rule, but then you'd be running at other kinds of bugs.
The order of hooks is only important insofar as they do not change between rendering. That's it. Hooks should always be called, and always in the same order.
A basic React Hooks component has way less boilerplate involved than a class component does, and hooks allow for much better code architecture when it comes to separation of concerns.
Functional components force your component lifecycle into a funnel where the lifecycle becomes obvious, because all the inter-dependencies are already there.
For componentDidMount, you can replicate this easily with hooks:
const Hello = () => {
const mounted = useRef(false)
useEffect(() => {
if (!mounted.current) {
mounted.current = true
console.log("Mounted!");
}
}, [])
return <>Hello</>
}
Extracting anything into a hook is trivial: function useMounted(f) {
const mounted = useRef(false)
useEffect(() => {
if (!mounted.current) {
mounted.current = true
f()
}
}, [])
}
const Hello = () => {
useMounted(() => {
console.log("Mounted!");
})
return <>Hello</>
}
Implementing an unmount callback is also trivial.The useMounted() example, coincidentally, shows a real benefit to hooks: Easy extraction of reusable logic. Hooks are like plugins this way. For example, one hook I've written that I use all the time is this [1], it lets you get a callback every time your component resizes. If you were to do this with a class-based component, you'd have to use an ugly HOC, which results in lots of unnecessary wrapping/nesting.
For componentDidUpdate, useMemo() is arguably easier to deal with.
[1] https://gist.github.com/atombender/b95bbd2014f6a3ec303b843a9...
export const useMounted = (f) => {
useEffect(f, []);
}
Edit: originally: export const useMounted = (f) => {
useEffect(() => {
f();
}, []);
} useEffect(f, [])My main complaint with hooks is people seem to think they always need them. I've seem components where every single piece of data, no matter how trivial, is slapped into a useMemo(). It becomes a lot of mental overhead to trace through them all, especially confirming the useMemos have the correct dependencies specified. Especially when the vast majority of the time, useMemo just isn't needed.
The low level problem here is your team doesn’t realize that every word of code means something.
You need to get past the idea that code has to be there to do a thing and get to the place where you expect every bit of code to explain something.
“Why is there a useMemo here”... if your team can’t explain that, then “changes requested”.
I've seen a lot of cases in the wild where useMemo was used to wrap closures, "for performance reasons", but that's obviously more wasteful than just using the closure by itself. I think a lot of people don't realize that.
That said, you're correct in that this should be the exception and not the rule. It's pointless to do this for a component that returns a single element, as the virtual DOM reconciliation will be much faster than the memoization + props comparison.
I've created a CodePen to illustrate: https://codepen.io/RussianCow/pen/xxbERLL
Open the console and watch as the "render child" message is logged once per second, showing that the child is re-rendered every time the parent is.
But on the other hand I agree that "keep the order!" is a very simple request for a feature that is so useful.
What do you mean?
I have no idea where this is implemented in React, but in Preact it's a simple integer that is incremented:
https://github.com/preactjs/preact/blob/abc162c4d6442d5495f6...
I think there are other ways to implement them, such as treating the return of the render function as an AST and turning hooks into dumb factories, like a free monad. But considering the constraints they have (the need to maintain order) and the way it works (the fact the render function runs multiple times), I'm guessing it's just the same solution as Preact.
With my hooks I've not gotten that visibility but that's probabbly just me getting used to hooks and how to write them better in a way that makes things as clear as the old lifecycle methods.
It doesn't help that I'm bit of a noob as far as web development goes and most examples I look at are pretty straight forward simple apps for teaching the concepts ... but my questions start when things start scaling up.
I really like how the post showed the contrast between "imperative anytime thing x changes remember to poke thing y" vs. "declarative thing y is calc'd on thing x".
That said, I'm a little disappointed with hooks b/c, while React is not at all claiming they invented this declarative approach, it seems like other "state-of-the-art" approaches (like mobx? not picking that one as the best/only, but it's observer-/push-based) can do the same thing without the ugly duplication between "here's my calc on x/y/z" + "oh and my calc's array of dependencies is [x,y,z] which btw also doesn't use deep equality".
Like I get that it works, and it's built into React, so it's a for-free declarative/derived value/derived state approach vs. pulling in yet another framework, but I'm disappointed they couldn't (yet?) solve the "you also have to specify your dependencies/shallow equality" problem.
There is a huge need for them to have abstractions which provide structure and consistency without being hidden behind too much magic. React in general, redux, and hooks, to me all work to fill this need in a way which is superior to alternative tools. Specifically you can learn how they work in ten minutes or less.
Of course ecosystem variety is critical and there are no doubt many cases where mobx, non-hooks, or similar implementations are the better solution.
Startups succeed for the exact reason that they avoid the "average" team structure that bigger companies employ. You need a small team of experts, not a standard team of juniors with a few seniors. Beating the averages and all that.
A good sensible developer, left alone, for sure will get 10x or 100x the work done in 5 years.
But also.... The senior developer has Seen Some Shit, and is psychologically damaged. They will resist doing things a stupid way that’s still “good enough”. The Jr. developer will happily follow orders.
That’s worth something too. Maybe the 10x comes back there.
Probably diversity is worth the most. And just cut out bad blood, without prejudice, when it appears.
It's harder to fix some classes of mistakes/performance issues in the more automatic systems, and the classes of mistakes in the manual dependency tracking world are often easier to spot ("why isn't this updating?" when forgetting a dependency as opposed to "why is this updating too often?" with automatic tracking).
I wouldn't be surprised if there aren't more automatic tracking tools that show up built on top of the basic hook pattern.
My biggest complaint with the React dependency tracking is that empty dependency array [] has different behavior from null/undefined dependency array and so far I always have to look up which one I actually need and I just haven't yet written enough hooks that I've internalized why which behavior is which. It keeps the hook functions terse to write, but it's a small papercut.
I wish they had used a constant, like "React.everytime" instead of null. I rarely want an useEffect to fire on every render, but wanting it to fire on mount is very popular, to the point a lot of people have an `useMount` custom hook...
[0] https://redux.js.org/style-guide/style-guide#use-immer-for-w...
Isn't this a constraint imposed by the language? If they could reflectively inspect the code making up a functional component, they could determine the dependencies, am I right?
1) Instead of taking a callback, can your hook return some value instead? I've found that in most cases, it can. That value can then be used to in some other (hopefully shorter) dependency array for a useEffect() hook that is a peer to your original hook.
2) In cases where your hook must take a callback, your hook should store the latest version of the callback in a local useRef. This add a lot more boilerplate code, but it feels safer to me than requiring the caller to manage the identity of the callback.
I've just had to make peace with the fact that I'm going to have to write a lot of boilerplate glue code around hooks. I try not to worry about it and I just hope the next version of hooks is better.
const useClickOutside = (ref, onClickOutside, isActive) => {
...
useEffect(() => {
if (!isListening && isActive) {
addHandlers(handler)
setListening(true)
} else if (isListening && !isActive) {
removeHandlers(handler)
setListening(false)
}
return () => {
removeHandlers(handler)
}
}, [ref, !onClickOutside, isActive])
}
I remember adding the negation here as the useEffect would fire needlessly (or wouldn't, can't remember), but wasn't entirely sure what it did. The whole hook is just for checking clicks outside an element.Anyway, I agree with you that at times they can a bit too abstract for the fellow programmer. Without prior knowledge how hooks work it can be quite tedious to read (without proper documentation at least)
(Something like <Blabla onClickOutsite={() => doSomething()} />)
Since a function is an object, and objects get compared by reference, a new function that does the same thing is not equal to the old function. Thus, this is a change.
Negating it turns that into a boolean, which gets compared by value. false is the same as false, so no change. Any type coercion (e.g turning it into a string) would've worked.
In this case, there's nothing specific to React or hooks, just plain old JS comparison.
Based on the name, it seem like onClickOutside will always be a function, so always truthy, so it's effectively the same as just leaving it out of the dependency array.
MobX does the same, automatically and under the hood. Vuex from Vue.js, likewise, but (currently) using Proxies. Svelte goes around the limitations of the language at compile-time.
Meanwhile hooks seem to be an attempt to write with performance in mind which in JS often isn't very effective and results in less readable code - unless you really know what you're doing.
I've noticed that even proponents of rewriting everything with hooks often don't fully understand how this thing works and because of that eventually write both less readable and less performant code.
My take is that this is the swan song of React. It solves a class of problems in React that are unheard of in other frameworks.
It really seems that way. If your only way forward for your library is to throw everything out and introduce an entirely new (and hacky) paradigm, then you know you're on some incredibly shaky ground.
The old lifecycle methods surely had their issues, but I feel I'm done with React. I'm at my limit. I feel like it was barely a year ago we were re-learning the new new lifecycle methods that were supposed to fix everything. Hooks are just about the most janky ass shit I've ever seen from the JS community.
I can't believe I have to write this out. But functions do not have state. Functions. Do. Not. Have. State. It's not functional programming. It's not even functions with closures. It's some mutant bastard that needs to be killed in the crib already.
Not only the author is not using anything like redux, the class examples look deliberately made longer than they should be. For example, one of them has 4 setState() calls after each other instead of combining them. Another manually performs deep equality checks for each key instead of using some library/helper.
The conciseness of React hooks also relies on the knowledge of implicit ordering of when certain functions are called, while lifecycle hooks are obvious and straightforward. Optimizing for writability is not the best approach for anything beyond prototype-level codebases.
That's not to say hooks aren't without a downside, it is more to learn, with a different mental model, and their own edge cases. But having used hooks exclusively for a larger project, I couldn't imagine going back to lifecycle methods.
class Chart extends Component {
constructor(props) {
super(props)
this.useEffect(() => {
const newData = getDataWithinRange(dateRange)
this.setState({data: newData})
}, () => this.props.dateRange);
}
}
You're not able to completely mirror the API (e.g. here the dependencies has to be a function), but you would get code which behaves very similar without the weird hook API. I'm sure these APIs would have various gotchas in order to be used correct, but it's not like React hooks is gotcha-free at all.Interestingly, useMemo doesn't even need to be defined in React in a class-based component:
function useMemo(f, dep) {
let prev;
let value;
return function() {
let next = dep();
if (next !== prev) {
prev = next;
value = f();
}
return value;
}
}
class Chart extends Component {
data = useMemo(
() => getDataWithinRange(this.props.dateRange),
() => this.props.dateRange)
)
}
This is also just one of the possible APIs. I'm sure there are variants of this which are more performant and/or easier to work with.But, the React team deliberately opted to only implement hooks for function components for a few reasons:
- Function components didn't have capability parity with class components in terms of having state and side effects
- Class components already had the ability to do this kind of functionality overall
- Long-term, function components are easier for React to deal with for things like hot reloading and the upcoming Concurrent Mode. By dangling a carrot to convince folks to move to function components + hooks, it becomes easier to implement that functionality for the React team.
> or example, one of them has 4 setState() calls after each other instead of combining them.
You would still need one line for each state property set:
this.setState({
data: newData,
dimensions: getDimensions(),
xScale: getXScale(),
yScale: getYScale(),
})
(Unless you were to compress all of that into one line...)In fact that makes things longer in terms of LoC. A bit less noisy though, that'd be true.
> Another manually performs deep equality checks for each key instead of using some library/helper.
Yeah well, I think this is completely fair if you want to "compare oranges with oranges". If the author were to include external libraries, then they would no longer be comparing "Reach with hooks" with "classic React", but with "classic React + some X library" instead.
But also in either the current or near future versions of react, with Suspense and concurrent rendering, the render method may be called multiple times over the course of a single render. So if any of those functions aren't entirely pure, there is a good chance there will be bugs from that.
Folks learning React will do that at first, because they don't know better. We _have_ caught that in code reviews, taught them not to do that, and moved on.
But yes, it is an actual thing I have seen people (coworkers and otherwise) do, not some made-up problem.
As far as I’m concerned, dealing with all your lifecycle events in one or two functions makes it much easier to reason about what is going on.
Because hooks look so much like magic, people treat them as magic and when they don’t work as expected, they have no clue why (adding more hooks to solve the problem!). I feel like Reacts’ surface area to shoot yourself in the foot has increased fivefold since the introduction of hooks.
The only thing I found really compelling about hooks is the ability to write shared functionality to include in multiple components, but practically I still have to find the first time I actually need that.
Maybe the code you work on is a special case, but in my experience that sounds exactly like an opportunity to refactor your code into custom hooks, as you mention in your last paragraph.
Compare the complexity of Hooks with the simplicity of state management in [Reagent](http://reagent-project.github.io/) or [Rum](https://github.com/tonsky/rum).
Complete ClojureScript example using Reagent:
(ns reagent-example.core
(:require [reagent.core :as r])
(def !count (atom 0)) ;; ! prefix is convention for state
(defn counter-component []
[:div
[:p "You have clicked the button " @!count " times."]
[:button {:on-click #(swap! !count inc)} "Increment"]])
(r/render [counter-component] (js/document.getElementById "app"))
The same example using Rum: (ns rum-example.core
(:require [rum.core :as rum])
(def !count (atom 0))
(rum/defc counter-component < rum/reactive []
[:div
[:p "You have clicked the button " (rum/react !count) " times."]
[:button {:on-click #(swap! !count inc)} "Increment"]])
(rum/mount (counter-component) (js/document.getElementById "app"))The primary difference is where the data _flows from_. Like another commenter pointed out, your examples can be done similarly as trivially with Hooks. The difference is that the state is stored in Reagent in a global mutable atom, while the state in React is stored in the React tree, and is tied to the render lifecycle.
This doesn't seem like a huge difference on its face, but in practice it makes for very different experiences navigating a code base.
In Reagent, your data is flowing down via props but also from the side via any number of atoms/subscriptions/etc. In React, you know that your data is always either state local to the component or passed via props.
This isn't even to mention the other various hooks like useEffect, which allows you to coordinate side-effects with changes in data in a composable, locally-scoped way that Reagent does not allow you to do. In order to coordinate side effects, it requires you to keep the description of side effects completely separate from your components, which makes them less composable.
For people who enjoy the way that React Hooks makes you think, and are also interested in ClojureScript (immutable data structures + amazing standard library + hot reloading + macros built into the language), check out https://github.com/Lokeh/hx or its WIP successor, https://github.com/Lokeh/helix (I am the author of these libraries).
One of the things that first drew me to check out reagent and hx was that the components are declared as data. I haven’t tried any of these libs out yet, so while hiccup syntax looks to me to be more idiomatic clojure, there must be solid, practical reasons for implementing them as functions that I haven’t understood yet, and I’m curious to hear more about what they are.
This is a constant performance tax on your application and makes Reacts tradeoffs re: virtual DOM worse. React Elements are already data (they are POJOs), so you lose nothing in terms of declarative-ness using hiccup vs. functions/macros.
If you're very attracted to hiccup syntax, you can still use hx's hiccup parser with helix (docs incoming) or try out a library I wrote called thump[0], which uses reader tags to parse the hiccup at compile time into React Elements.
const Counter = () => {
const [count, setCount] = React.useState(0)
return (
<div>
<p>You have clicked the button {count} times.</p>
<button onClick={() => setCount(count => count + 1)}>Increment</button>
</div>
)
}
Replace the atom/swap with a useState hook and it's almost identical.useState returns 2 variables - a getter and a setter, where the getter is just a variable and not even a function to call.
How is that equatable to some hidden construct? How is Clojure superior to that?
import {createElement as h} from "react"
const Counter = () => {
const [count, setCount] = React.useState(0)
return h("div", {},
h("p", {}, `You have clicked the button ${count} times`),
h("button", {onClick: () => setCount(count => count + 1)}, "Increment")
)
}
There are additionally libs like hyperscript (https://github.com/mlmorg/react-hyperscript) that take the non-JSX approach further. Although honestly, I don't recommend it. JSX is really great even it seems a little unfamiliar.The '1 language' version requires you to learn language-specific syntax for creating HTML elements.
How is that more compelling?
(ns qlkit-example.core
(:require [qlkit.core :as ql]))
(defcomponent Counter
(state {:count 0})
(render [atts {:keys [count] :as state}]
[:div [:p "You have clicked the button" count " times."]
[:button {:on-click #(ql/update-state! update :count inc)} "Increment"]]))
(ql/mount {:component Counter
:dom-element (js/document.getElementById "app")})Maybe I'm just a dinosaur, but I can't keep up with all the changes to JavaScript, whereas ClojureScript compiles to ECMAScript 3 and the syntax hasn't changed since its release 12 years ago.
I'm not aware of such solution in reagent or rum. Something like re-frame comes to mind, but there, all effects are global.
Deal with the state somewhere else. I don't want to have to reason about when a component mounts, how many ways it could trigger things, that will cause itself to render again and trigger more unexpected things.
Why did a component mount? Maybe the route changed, well I will just fetch data where I handle that event. Maybe I got an API response, well guess what I can do stuff right there.
All I had to do was let go of the idea of "component". I know, we need to make layers, and isolate modules, separation of concerns. But "components" are unsatisfactory for me, because all I ever got was implicit dependencies. Sure this component deals with his own data, and its children do the same, but somewhere, they DO have to talk to each other at same point, always. The component depends on the user, or user's list of thingies, and bam I've smeared it all across all of them. Because I'm building an application, not a component catalog.
Also testing is a PITA. I think most of the time it's useless to test a component without the whole app, and then it's an integration test, but you really want an e2e test by then.
I tend to agree. With a couple of caveats:
On my team we have visual tests that use factory data to just make sure different states of each widget look right. I think it’s pretty valuable as a safety net for CSS refactors. We use Chromatic for this, but they charge per state, so even a medium sized app will get into the $500+ range per month.
I also think that, while in practice every front end I’ve worked on is tightly coupled to the backend (making front-end only testing pretty useless), if you build an “optimistic UI”, you can do it.
We got close to this at my last company, where we used Vuex to implement a kind of a simple dB on the client, and allow server syncs to kind of build up in the background as needed.
You still need to stub some initial requests, but you could test a UI by doing that and then kind of “coasting” in the front end. Eventually you’d hit an interaction that needed the server, but in this way you can test some short sequences of interaction, which is probably enough to test the basic interactions, and leave the E2E tests for actually making sure the front end is wired to the server, db, work queues, etc correctly.
So we are removing Classes in favor of Functions.
Something tells me that on the next iteration we will see something like this:
"In a declarative component, we just need to declare what UI elements shall stay in-sync with what items in data model."
And that is precisely what Angular is based on (declarative two way binding).
And the cycle of evolution spiral will be complete.
Life goes on ...
They require Zone.js stuff in Angular and use of ES2015 Proxies in Vue.js. React has neither of these.
Hooks is basically a different syntax describing the same framework.
I personally love them since I no longer need to care about lifecycle methods, but can describe state and state change logic.
1. The comparison of the lengths of the function using the two styles (class/function)
2. The comparison of the length of the changes required in the two styles
3. The clarity achieved by maintaining the two columns consistently distinct.
If you are interested in how to demonstrate the efficiency of a new coding technique -- even if you're not interested in React Hooks -- this is worth a read.
I’ve seen several bugs especially with useRef/memo, and animation related hooks, or hooks that combine several together to expose a simpler api.
I didn’t notice them until working with larger file uploads and local content. They can be hard to spot with small changes, Since they are usually deep in the memory trace stack.
The component with the bug doesn’t need to even be in the tree to accidentally retain state that you’ve cleared/unmounted.
Since then I’ve made a simple test that saves a large test blob to local state, and makes sure it gets garbage collected etc, not stuck in another components previous state memory snapshot.
—
An example could be a loading animation above/outside the tree, that uses a hook somewhere inside its library that tracks browser size changes. It wouldn’t seem to affect you, but it could inadvertently cache some state in the window. So you’d only detect it if you’re working with very large content.
componentDidMount() {
const newData = getDataWithinRange(this.props.dateRange)
this.setState({data: newData})
}
componentDidUpdate(prevProps) {
if (prevProps.dateRange != this.props.dateRange) {
const newData = getDataWithinRange(this.props.dateRange)
this.setState({data: newData})
}
}
Could be: componentDidMount() {
get(this.props.dateRange);
}
componentDidUpdate(prevProps) {
if (prevProps.dateRange != this.props.dateRange) {
get(this.props.dateRange);
}
}
get(dateRange){
const newData = getDataWithinRange(dateRange)
this.setState({data: newData})
}
It removes the duplication.> The class example is misleading in terms of the line of code.
It doesn't matter anyway. With or without the function, the class example compares unfavorably to the hooks example.
I am not against Hooks, but I do not believe the example portrait the situation properly.
Initial react was super simple. A render function that renders a virtual dom tree. It’s diffed against real tree. View is simply a function of state. You could build a simple react in an hour and show the concepts.
New react feels doesn’t share the same ethos of radical simplicity.
Or are you just talking about “pure” functional components? How would you build a full featured application with just those?
I think it’s actually _less_ magical now, because of the required static order of hook invocation. This implies that each component instance has something like an array backing it that keeps track of hooks calls between renders. This array maybe could hold said state value at the index corresponding to the static ordering of that useState call. So then maybe useState just populates the initial value in the array and provides a callback which both sets the value in the array and then queues up a component re-render
React uses simple objects called "fibers" to track metadata for every component in the tree.
When you begin rendering a function component, React creates a linked list of hooks, and attaches the linked list to the fiber so it knows what hooks exist for that component.
Each hook can save some kind of value in the linked list. For `useState`, it's the state value. For `useCallback` or `useMemo`, it's the cached value, and so on.
When you render again, React loops over the hooks linked list, reads the value at that index, and returns it to your component.
Shawn Wang has a talk where he builds a tiny version of hooks in a few lines:
Honestly I don't care at the moment since I'm a user and not the developer of the framework.
But hooks feels so much simpler for me - I can reason about what the component does instead of what framework lifecycle methods it goes through.
It isn't, it's diffed against the previous virtual dom, to create a set of patches, which are then applied to the real tree. This is why modifying the dom directly through refs can result in bugs.
Also worth flagging this great touch-- scroll-aware "scribble"/cross-out animation: https://imgur.com/a/mhPCjNy
function Hello() {
// other stuff
const callback1 = () => {
// do stuff
}
// other callbacks, effects, etc
return <button onClick={callback1}>{label}</button>;
}
See how the callback functions, and everything else, is defined before the "rendering" part of the function? Compared to, say: function Hello() {
const render = () => <button onClick={callback1}>{label}</button>;
const callback1 = () => {
// do stuff
}
// other callbacks, effects, etc
return render();
}
That uses the Most Significant Function First approach, where the higher-order function (render) is defined first, and the functions it depends on are defined after them.That latter example, AFAIK, compiles fine (filling in the elided etcs, of course).
To me, MSFF reads better. The higher order function being defined first gives the reader a lay of the land. They provide the highest overview of what the module as a whole does. The functions that follow drill down into increasingly finer details.
LSFF is backwards, forcing the reader to understand the finer details of a module without having any clue about their context or the overarching goal until the very end of the code.
React specifically, as a reader, I want to know first "What are we rendering?" Only then do I want to then know where the data that goes into that render comes from, the processing of that data, and what user interactions there may be.
I see LSFF a lot, especially in React code. Maybe it's just a personal preference, but I just feel like most coders are jumping straight to the "render" part of the code anyway, before looking at the rest of the module. So why write it at the bottom? Why not write the code the way it should be understood?
function doThing(target) {
console.log(myVar);
const myVar = target.prop;
}
Because that's invalid Javascript. React Hooks are just functions that return markup.If your goal is simply to see what it renders first, that's fine, you simply need to look at what the function returns.
I disagree with your assertion that LSFF is 'backwards' - it is simply not what you are used to.
MSFF could definitely be argued for class-based components, but it's simply not an option for hook-based components.
function doThing(target) {
function run() { console.log(myVar); }
const myVar = target.prop;
run();
}
The console.log's evaluation is delayed, so it's totally valid.I picked up web development 1.5 year ago and already went through several stages of re-learning. It's great that the ecosystem is developing this rapidly. But it does require significant effort to stay on top of the latest techniques. Especially if you're not a full-time developer.
For instance a simple custom hook use_graphql_list(url,query) can encapsulate the entirety of fetching json via a graphql query keeping an array up to date and in sync and present data in a format that is easy to use in a list component.
For instance in rust:
let graphql_query = "{countries {name currency emoji}}";
let graphql_url = "https://countries.trevorblades.com/";
let container_name = "countries";
let (list, graphql_control) =
use_graphql_list::<Country>(
graphql_query,
graphql_url,
container_name);I learnt react this year; I spent quite a bit of time researching and reading, and obviously there was a _lot_ of ground to cover. In that process, it was really helpful to know whether what I was reading was published a month ago or three years ago.
The first words on that page (after the title) are "React introduced hooks one year ago". That may be true today; it won't be in a year, but in a year somebody looking at this page will probably see exactly what I'm looking at now. Not showing a publication date does that somebody a disservice.
</moan>
In this case, there's only 1 result… the author's public repo. So I guess the search worked! But the theme seems to be a personalised one the author created themselves.
But the primary colors remind of Solarized Dark, if that helps: https://www.google.com/search?q=solarized+dark&tbm=isch
This is nowhere to be found in the hooks approach, without using flags and other strange constructs.
Where is it gone ?
const [thing, setThing] = useState(true)
useEffect(() => { console.log({ thing }) }, [thing])
Here's a helpful diagram that shows the difference between render and commit phases in terms of the class lifecycle methods: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram...
So when a render executes useEffect with a callback (and values in its dependency array has changed), it will schedule the callback to be run once React has committed the DOM changes based on the current render.
Basically `useLayoutEffect` fires in the same phase as `componentDidUpdate`, but it's discouraged unless you really need that timing (to do things like read the DOM directly and/or trigger a re-render before paint)
https://reactjs.org/docs/hooks-reference.html#uselayouteffec...
Good luck with that.
function Counter() {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(() => setCounter(n => n + 1), 100);
return () => clearInterval(interval);
}, [])
return (
<div>{counter}</div>
);
}https://reactjs.org/docs/hooks-reference.html#functional-upd...
import React, {useState, useEffect} from 'react';
const CounterComponent = React.memo(() => {
const [counter, setcounter] = useState(0);
useEffect(
() => {
let counterTimer = null;
const incrementCounter = () => setcounter(counter + 1);
// counterTimer is null if it has not yet been started
if (counterTimer === null) {
counterTimer = setInterval(incrementCounter, 1000);
}
// Remove event listener on cleanup - note the remove function signature needs to match the add function sig
return () => clearInterval(counterTimer);
}, [counter]
);
return <span>{counter}<br/></span>;
}); export {CounterComponent};counterTimer is always going to be null where you're checking it for null.
You can't really do it properly without also using useRef to track the value. The reason is that the object returned by useRef is always the same, so it's never stale.
The key thing to understand about doing this is to set the state in a context, and do the displaying in another component. There's no point in trying to get the component to re-render when the time value updates. That's the wrong way to program react.
You've already mentioned in the other comment, that drift is your concern, and that can be fixed easily. Though it has nothing to do with hooks. One has to track the time when the clock starts, and query current time on every tick.
As for creating a setInterval() and tearing it down after every render, you can simply pass an empty deps list, instead of declaraing counter variable as a dep in the array. Or, if you want to keep ESLint happy, declare counter as a dep, but use `setTimeout()` instead of `setInterval`
I took a stab at this (React with TS):
import * as React from "react";
const TICK_INTERVAL: number = 1000;
function App() {
const [counter, setCounter] = React.useState<number>(0);
React.useEffect(() => {
const startTime = Date.now();
const timer = setInterval(() => {
setCounter(_ => {
const timeNow = Date.now();
return Math.round((timeNow - startTime) / TICK_INTERVAL);
});
}, TICK_INTERVAL);
return () => {
if (timer) {
clearInterval(timer);
}
};
}, []);
return (
<div className="App">
<h1>{counter}</h1>
</div>
);
Codesandbox link with working demo: https://codesandbox.io/s/amazing-solomon-mf3r6But this code wouldn't be that different if you wrote it in class fashion, except you'd be making a few of calls to this. Most of this would go in componentDidMount and the cleanup would go in componentWillUnMount
Where hooks really shine, is if you want to add / extend this functionality.
Say, you now want the counter to pause when you're not looking at that tab, and resume once the browser tab is in focus. Imagine if you had a pageVisibility API hook, and it returns true and false accordingly, based whether or not the tab is visible at any point of time or not.
In a real-world scenario, it'd not be a basic counter, but maybe an API polling for real time data, and user don't want the page to keep on polling when not in focus.
In that case, you'd have two changes: one call to the useVisible hook, and one more to pass the boolean output of this hook into your Effect hook.
const TICK_INTERVAL: number = 1000;
function App() {
const [counter, setCounter] = React.useState<number>(0);
const startTime = React.useRef<ReturnType<typeof Date.now>>(Date.now());
const isVisible: boolean = useVisibility(); // assume taken from NPM
React.useEffect(() => {
const timer = setInterval(
() => {
if (TICK_INTERVAL && isVisible) {
setCounter(_ => {
const timeNow = Date.now();
return Math.round((timeNow - startTime.current) / TICK_INTERVAL);
});
}
},
isVisible ? TICK_INTERVAL : null
);
return () => {
if (timer) {
clearInterval(timer);
}
};
}, [isVisible]);
return (
<div className="App">
<h1>{counter}</h1>
</div>
);
}
Working codesandbox demo: https://codesandbox.io/s/heuristic-mcnulty-oo58zNow, let's go one step further, and add local-storage / indexed DB for storing these values, on page close. If you close the page, and re-open, it should resume from where it was before closing, and then count up from there - not zero.
All we need now, is another hook, that abstracts away that storage interaction, storing on window.unload and componentWillUnMount, and retrieving value from storage when component mounts for the first time.
After this point, it'd be illuminating to look back and try to combine these three functionalities, using class components with HOC or Render Props pattern; and think about the effort it'd take to decouple and reuse the count-up logic, from the page visibility logic, from the storage logic.
Edit: formatting
const data = useMemo(() => (
expensive(a,b,c)
), [a,b,c])
Now image a,b,c all change at the same time in response to a button press. How often is expensive() going to be called? 3 times? That will take too long. 1 time? That's unpredictable. setA(1)
setB(2)
setC(3)
in a single click handler, there would be a single render, and a single call to the `useMemo()` callback.On the other hand, if the state setters were called outside of a React event handler (such as a `setTimeout` or somewhere in an async function), each of those would cause a separate render pass, and the `useMemo` callback would end up being called 3 times.
Note that in the upcoming Concurrent Mode, React will batch updates across event ticks by default.
Also, and this is a totally minor quibble, but I hate it when websites have that tiny bit of horizontal scroll. The animations for code size were very pretty though.
Edit: useEffect performs a side effect every time one of it's dependencies has changed value. Passing an empty array means the effect is only run once (on mount) because there are no dependencies.
Disclaimer: got nothing against hooks but hate false examples.
Anyway I left React/Preact/Inferno/Vue for Mithril and my life is a lot simpler now.
On my MacBook, fullscreen, I get horizontal scrolling on this page. Honestly, what kind of monitor was this site designed for?
The content is excellent, though.