I have not been a fan of hooks thus far, but these are some intriguing examples. Especially the useMemo ones.
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.
You use the word "simple," but it sounds like you mean something more like "familiar." That's not a knock; familiarity is important when designing an API that thousands of developers are going to use every day.
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.
To play devil's advocate a bit: I don't think you were wrong the first time.
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.
I'd argue it's more complicated by way of how much confusion I see with newbies. The old class-based lifecycle+methods was pretty straightforward - it only looked complicated at a glance. Hooks on the other hand do enough magic in the background that each one seems to come with its own set of misconceptions that have to be corrected.
The ironic thing is that hooks are marketed by the react docs as being the solution to avoiding the "difficulty" newbs have with classes.
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.
Should probably dump it here myself: My experience is mainly with co-workers (years of experience) and new hires (just out of college/bootcamp) who can code, but haven't yet used React.
Yeah my experience is pretty much all with beginners (usually fresh out of a bootcamp or college, or switching from tangentially related industries like CSE).
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 think the old way is simpler in the sense of building on fewer concepts, and therefore allowing an easier learning curve, if you grant that understanding the React component lifecycle is required and inevitable. Once you understand the lifecycle, running code when certain lifecycle events happen is a very small, concrete conceptual leap. Names like "componentDidMount" tie directly into your fundamental understanding of React. If using them to maintain state forces you to deepen your understanding of the component lifecycle, that effort will pay dividends when you have to debug, optimize, understand someone else's wacky code, etc.
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.
I think this also falls into the same familiarity trap as the parent.
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 think the old way is simpler in the sense of building on fewer concepts, and therefore allowing an easier learning curve
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.
> 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?
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.
I think the argument that hooks lose on simplicity (as opposed to familiarity) can hold water if you think about it this way: sure procedural code is very familiar to most JS devs and consuming hooks indeed feels procedural. Ok. BUT. Reasoning about the rules of hooks when you're writing non-trivial hooks, that's not familiar, and it's not simple either. At my day job, there have been some debates about supporting Suspense composition and the semantics are very difficult to reason about because not only you have to keep in your head what the procedural code actually does, but you also need to keep track of whether e.g. certain flavors of certain expressions violate the rule of hooks. The term "magic" is a very appropriate way of describing how hooks feel in this context.
Thanks for sharing. As someone who's done React for years (and webdev since 1998) but yet to do more w/ Suspense than read about it, it seems to me like [Suspense + Hooks] is an area where we're still some distance from solid idioms and established best practices. Maybe the APIs will undergo further refinement before that happens. As for magic (which, if good, decomposes into sane primitives), you might be right. But in general I appreciate the degree to which React's evolution over time has trended towards reducing the cognitive overhead of lifecycle concerns and state mgmt solutions. (To say nothing of reconciliation, which I'm perfectly happy to enjoy, whether it's voodoo or not.) /ramble
> familiarity is important when designing an API that thousands of developers are going to use every day.
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!
My initial impression was similar to yours, but after I started using hooks, my opinion changed 180 degrees. I've been slowly converting a rather large React frontend to hooks and it solves so many problems.
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.
Hooks definitely bring a different set of tradeoffs, especially compared to Higher Order Components. I'm a Redux maintainer, and the React-Redux API is a good example of this. The `useSelector` and `useDispatch` hooks are way easier to statically type than `connect` is, but calling those hooks embeds knowledge of Redux directly into the component instead of separating it.
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.
Totally agree about useState and useDispatch... So much easier! It is true that it beds knowledge in the component, but how likely am I to replace redux in a working app?
Separate methods for lifecycle events is probably the biggest weakness of class components. They work well for smaller components, but when you have multiple things going on in a single class you have code related to a single concern scattered around multiple methods, and you start having to use conditionals inside your lifecycle methods.
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.
I would totally disagree. We started migrating our front end to React this year. We dipped our toes in with Class based components, found out about hooks, and never looked back. Hooks are confusing when you're trying to translate a class based component to a functional component with hooks. Starting a component or page from the ground up with hooks is so, so much simpler than class based components. As with any iteration of any framework, there are gotchas, but they are no worse than Class based react (totally opinion there). The mental model, once you latch on to it, is so simple.
After using hooks for a while, I have to disagree with most of your points. The issue is not clarity or simplicity, it's familiarity. Once you start actually using hooks, things make a ton of sense. It's a different way to think about components, not an inherently more-complex way.
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.
I have the opposite view. The things you mentioned (componentDidMount, etc.) are actually hard to get right, and class-based components don't help by scattering the logic all over the place.
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:
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.
PureComponent is reproducible with React.memo(functionComponent)
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.
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.
To be fair, if you're passing the closure to a component that extends PureComponent or is wrapped by React.memo, then memoizing the closure will ensure you take advantage of that optimization in the child; otherwise, you're passing a branch new function reference on every render, which means the child is needlessly re-rendering. At work, we have many components that are very complex but take a limited set of props, so these kinds of optimizations are absolutely vital for good performance.
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'm not sure why you think it wouldn't. Any time a component renders, React also renders all of its children except the ones for which shouldComponentUpdate returns false. If that is defined by using PureComponent or React.memo on a child, then you have to make sure that the props you pass in are not going to change on every re-render (i.e. you can't pass an un-memoized callback because a new function reference will be created on every render).
Lifecycles map closer to how the underlying UI layer works, but not to developer expectations. I've lost countless hours to desynced implementations such as failing to account for componentDidUpdate when overriding componentDidMount or missing a corresponding componentWillUnmount cleanup.
I’m genuinely wondering why the fact that the order of hooks needs to stay the same between successive invocations of your function component is considered a bad or confusing thing. The complaint comes up a lot, so I know that it bothers people, but I haven’t been able to understand why.
I can sympathize with people that find it confusing, because it is an unusual requirement considering they're simple function calls. The fact that the only way to implement them (that I can think of) is messing with global state is also a very strange in today's functional-centric programming world.
But on the other hand I agree that "keep the order!" is a very simple request for a feature that is so useful.
A regular global variable that is incremented each time you call useState/useEffect/etc and is resetted every time a new component starts rendering. This is why hook order must be maintained.
I have no idea where this is implemented in React, but in Preact it's a simple integer that is incremented:
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.
Resetting it as render starts makes that number conceptually local to the vnode, even though preact's implementation involves a variable that's global in the context of that module.
You don't have to use hooks and class declaration syntax is not going away. If you don't like a paradigm, you don't have to use it. I was quite skeptical of hooks at first, but after using them for a bit, they grew on me. It is not as simple as the class-based approach, but I also haven't found it to be a giant mental leap: my "warming up" to hooks took about a week.
I've just used hooks for the first time, I'd been working with react for three years before this, but let me tell you, using graphql/apollo with hooks is such a simple thing, I can't go back to using class components, it's so much simpler like this, loadables are a great concept.
I love hooks for most things, but what I still struggle with are big higher level components that hosts a bunch of smaller components where I can see componentDidMount, update, etc.... and get a feel for the behavior overall.
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.
What an amazingly well-written, well-illustrated blog post.
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.
People misidentify one of the biggest challenges of building a SPA in a startup, small, or medium, sized org: taming the horde of junior developers coming into front end development via self-teaching, bootcamps, and CS programs, who more often then not are writing significant parts of the product to be shipped.
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.
I think you're totally right, but just want to point out. I'd never, ever rely on junior devs in a startup, for this exact reason. I can't think of a better way to totally ruin yourself than risk that. Build it yourself until you get funded and can afford the $200k or whatever the market rate is for experts.
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.
It does seem crazy to me too! At best the jr developer is what, half price? 1/3?
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.
I've worked with frameworks all over the map in terms of dependency tracking, and right now I'd lean toward React's manual dependency tracking is probably the better approach as the default. Making it manual does mean that it is easy for silly mistakes to be made (missing a dependency or adding one too many), but it's a learning/debugging trick and eventually starts to become second nature. Tools can be built on top of this manual tracking to better automate it, such as edit/compile/pack time linters ("warn: you use this variable in this callback but don't include it as a dependency"), or additional meta-hook libraries at runtime that do some of the automatic dependency tracking magic (through Proxy objects or what have you).
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.
> 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
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...
Not using deep equality allows for huge performance gains. You can use an immutability library like https://github.com/guigrpa/timm to handle mutations, if you want to edit a nested property on an object and return a new object.
I haven't heard about timm before, but it looks very similar to immer https://github.com/immerjs/immer, which is even recommended in the official React docs now
Nice, thanks to both of you for the tip. I always hated ImmutableJS's clunky API which is partly the reason why I switched from Redux to Mobx as the object assigning felt otherwise so icky. I have to check out if Immer is any better =).
> solve the "you also have to specify your dependencies/shallow equality" problem
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?
Reasons I dislike hooks: The dependency array feels like a hack. It's way too easy to accidentally make an infinite loop. It's no longer safe to pass around functions; they have to be wrapped in useCallback(), which also has a dependency array. Yes, I'm aware of the eslint rules and the autofixer. It just feels that I'm so far away from writing javascript. Hooks are great when you're doing a simple setState or something, but with complex logic they get confusing quickly.
Yes, I have had the same frustrations. The dependency array pattern feels very brittle. I have found two ways to deal with this:
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.
Your comment reminds me, I never found a perfect explanation what a negation inside the dependency array does. Does it simply negate the checked variable or just omits it from ever triggering eg useEffect? For example:
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)
In this case, onClickOutside was probably a function that you defined inline in your parent component, which would actually be a new thing on each render.
(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.
Negation just includes the opposite boolean value in the array. _If_ that value at that index happens to be different than the last time the component rendered, the effect will run again.
The negation is effectively just a cast to boolean in that situation. If the value changes from truthy to not-truthy or vice versa, then the effect will run again, otherwise it won't.
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 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.
The class-based examples given are a bit disingenuous.
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.
I don't think it's fair to say React hooks are optimising for writability, even if they are much shorter, but modularity. While lifecycle methods are 'obvious and straightforward', they often spread related functionality across multiple different functions, which can be more easily grouped together with hooks. And if you have similar lifecycle-based functionality that needs to be shared between multiple components, you end up with a variety of not-great solutions, whereas that is simple with hooks.
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.
All of these discussions seems to forget that React hooks is two different things combined: (1) it's a new magic way of having stateful components and (2) it's a new set of APIs. I don't quite see why they _have_ to be so coupled. Wouldn't this be possible as well?
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.
Would it be _possible_ to implement hooks as part of classes? Sure, it's just software, code can implement basically anything.
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.
(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.
Why not put the function calls into the render? And only decide to rerender on state change. You could push nearly all that logic back into extends Purecomponent
They point out that the get* functions are supposed to be "computationally expensive", and you don't want something like that in your render method.
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.
Redux adds complexity as well, but I agree that the code looks to be much more repetitive than is necessary. I have never seen multiple calls to setState put one after another like that and it does seem disingenuous to make it look like people write React class components that way.
I've seen a _lot_ of folks put multiple separate `setState` calls in a row, one for each field they want to update. I make sure to point out that it's nicer to read if they're collapsed into a single call, but it's a very common thing.
A scrum master or maintainer needs to proofread and prevent this. That is is the purpose of code review. It’s too bad that repetition does not stand out as a mistake to so many with whom you have worked.
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.
A well structured argument should take the strongest interpretation of both sides. Having code quality/clarity on one side of the argument, while omitting it on the other, makes it more difficult to conclusively draw an informed opinion.
I just don’t get hooks. People say it’s an easy way to keep your lifecycle events close to the functionality they’re dealing with, but in the end it’s just a clusterfuck of function calls in the render method.
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.
> People say it’s an easy way to keep your lifecycle events close to the functionality they’re dealing with, but in the end it’s just a clusterfuck of function calls in the render method.
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.
I think that hooks and Reagent developed somewhat independently; or at least, they are not as related as one might think at first glance.
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).
I have a tangential question for you about helix. You switched from hiccup-style lists in hx to functions in helix.dom. I was just looking at fulcro and they made a similar choice. What drove that decision for you?
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.
Performance, primarily. Hiccup that is generated _at runtime_ is incredibly wasteful; you allocate a bunch of immutable vectors and maps, then parse them into React elements and throw them away, every time your component is rendered.
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.
The difference is in how the state is handled. In Clojure, it is contained in an atom, which is a well documented language primitive which can change value in controlled manner. It's not some magic construct hidden behind innocently looking JS function useState().
To be fair, reagent uses it's own implementation that satisfy atom protocol. And it uses some funky magic with keeping derefs and triggering rerenders. In principle, as atom is idiomatic Clojure, setState is idiomatic javascript. ratoms and hooks both add some sugar to it.
I'm not sure how far back you want to go, but `this.set` is used in most frontend frameworks, starting around knockout and backbone. You had object which managed state for you (then it was model, today it is component) and it had some setter method.
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.
FWIW, here is the same example in clojurescript qlkit. It doesn't use "reactive atoms", just the usual state map approach. Of course qlkit's selling point (like for Om) is when global state needs to be synchronized to components, which isn't an issue in this example.
If you need to manage state and want to focus on functions instead of objects (classes), try ClojureScript - especially in combination with [DataScript](https://github.com/tonsky/datascript), an immutable database and Datalog query engine. Clojure/ClojureScript's persistent, immutable memory model makes it much nicer for reasoning about state.
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.
Reagent atom is only useState part of hooks. useEffect is all about syncing effects with state/props changes. VDOM is this one specific, privileged thing so it can use return value of the function. Less common effects must use something different.
I'm not aware of such solution in reagent or rum. Something like re-frame comes to mind, but there, all effects are global.
I prefer it when render functions are pure and just render. Given some input, produce html that can dispatch some messages.
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 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.
Two way binding is absolutely awful and what React hooks appear to do is one way data binding which has always been considered superior. Of course one way data binding isn't exactly new but your comment is making some fundamental mistakes.
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.
One thing to watch out for currently are memory leaks that retain previous state even if unmounted and/or unrelated.
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.
The class example is misleading in terms of the line of code. The first example would have the componentDidMount and componentDidUpdate to call the same function
And the 12 LOC example allows you to change the logic in one place and not two. While more verbose, this does make it harder to introduce bugs by forgetting to update the logic everywhere it is used (copy/pasted)
Yes, it's a better pattern, but then the hookophobes would complain that the example artificially inflates the line count by adding a not-strictly-necessary function. I don't think the author is being disingenuous, just trying to keep the examples straightforward.
It doesn't matter anyway. With or without the function, the class example compares unfavorably to the hooks example.
The hook functions need the knowledge that the mount and update are doing the same thing. However, the example would be more complete if the mount was doing something different. In that case, not only the class example would be more clear to read with explicit function for mount and update but also would be similar in terms of the line of code. With Hooks, if you want something different you need to do a "trick" which is less obvious than defining `componentDidMount`.
I am not against Hooks, but I do not believe the example portrait the situation properly.
Hooks are incredibly confusing because of the underlying dark magic. I’ve had multiple people try to explain it to me but when I ask what does useState do under the hood? Can you write a simple version of it, they kinda get stuck.
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.
I’ve see this argument before but I don’t get it. How is this.setState any simpler? Can you explain how class based components work in any detail? For example, why do we need to call super(props) in the constructor? There’s a whole Dan Abramov post about that one (https://overreacted.io/why-do-we-write-super-props/)... is that really so straightforward?
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
I can describe both of those pretty simply (I think).
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:
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.
In the few cases I've used them, I've liked Hooks. I just have one big pet peeve with them. Everyone seems to write hooks in Least Significant Function First coding style. This is where you define dependent functions before the code/functions that call them. A short example:
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?
The pace at which React is evolving is both exciting and frustrating. There is so much you can do with all these new tools, but your code is never really up to date.
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.
I wouldn't say that the pace is too fast, it's just that you started at the right time :) Hooks are by far the largest change ever in React, but that happened after many years of stability.
The thing I like about hooks is that components can be truly modular in terms of both behaviour and state. They can so be relatively easy to reason about in isolation and therefore it's easier to reuse them with less plumbing needed.
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);
Not a comment on the content, but I really wish people wouldn't publish blogs without an obvious publication date.
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.
From this thread it seems a lot of people aren't very familiar with React Hooks yet. I highly recommend diving into it. useEffect can replicate lifecycle methods in a way that's much more sensible and easy to read if done right (in my experience of course).
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.
Is there a way to trigger something after state changes ?
In the class based components, setState has a second argument that gets triggered after state has updated.
This is nowhere to be found in the hooks approach, without using flags and other strange constructs.
Not that it matters too much given the question asked, but am I right to think that the useEffect callback would get fired on the next render (function call) here?
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.
And to add on to the other commenter, there's also a `useLayoutEffect` hook which fires synchronously after a react render but before the browser paints the 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)
Excellent tutorial for person familiar with React lifecycle learning basics of hooks... but mostly came to comment on the exceptionally high quality of the teaching and format of the tutorial. I especially love the "diff view".
Try creating a hooks-based component that increments an internal counter every second, using useState to track the counter and setInterval inside useEffect to start the timer on mount and stop it before unmounting.
Nope. Since the dep array is empty, the effect only runs once after the first render, & the cleanup function returned from the effect only runs on unmount.
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>;
But you're setting up and tearing down an interval each time counter changes, so it's no better than setTimeout. Compared to a single setInterval, it's going to gradually drift over time.
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.
I'm not sure why you believe this would be a difficult task to achieve.
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`
But 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.
Now, 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.
This looks like dataflow programming to me and the big downside are the so called "glitches". We want to react to every change but at the same time we want some changes to happen simultaneously.
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.
React batches updates that were queued from within its own event handlers. So, if you had:
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.
Hmm, how do you allow for more complicated updating schemes? Like if I want to update on something other than a shallow comparison? Correct me if I'm wrong, but the dependency array seems to use shallow comparisons.
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.
I like React Hooks, I like the simplicity. I dislike that it makes reading the code much more difficult. The code required to do a simple "componentDidMount()" is very abstract. It is difficult to reason about what a component does at first glance, with the class methods this is much easier.
To do 'componentDidMount' you simply need a 'useEffect' with an empty (non-null) dependency array. It makes sense to me - maybe the issue is more familiarity than simplicity?
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.
class components have a huge downside in that `this` is mutable. Hooks solve this problem, and this problem seems to have motivated their creation to some extent. This blog post goes into more detail: https://overreacted.io/how-are-function-components-different...
The this.setstate spamming and lack of using something like the PureComponent class show that the author either is bad at coding in class based react or intentionally making it look worse.
Disclaimer: got nothing against hooks but hate false examples.
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.