> Hooks addressed class component pain but introduced new kinds of complexity: dependency arrays, stale closures, and misused effects. Even React’s own docs emphasize restraint: “You Might Not Need an Effect”. Server Components improve time-to-first-byte, but add architectural complexity and new failure modes.
There are a lot of valid criticisms of React, but I don't think this is one of them. These problems are not really new with hooks. They're actually problems which existed in some form in the class component API. Taking them one at a time:
Dependency arrays: I cannot count the number of bugs I encountered which were due to a lifecycle containing some code which was supposed to be called when certain props or bits of state changed, but completely forgot to check one of them.
Stale closures: the second argument to setState allowed this exact bug. Also, people would bind methods in incorrect spots (such as in lifecycle methods) which has the same result.
Misused effects: at varying point, class components had access to the class constructor and the lifecycle methods componentWillMount, componentDidMount, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, componentDidUpdate, componentWillUnmount (this is from memory and is definitely only partially complete). Misuse of these was incredibly common. An article like "You Might Not Need an Effect" but titled "You Might Not Need Lifecycle Methods" or "You Might Not Need the Second Parameter to setState" would have been very helpful in the past.
Hooks reduced the number of opportunities for making mistakes, make enumerating those opportunities easier, and, critically, made them easier to detect and warn users about.
I'm not sure why you think you're disagreeing as my entire comment was explicitly about how the issues people blame on hooks existed before, but that hooks changed how they were presented/encountered. I literally began it with: "These problems are not really new with hooks. They're actually problems which existed in some form in the class component API."
As the rest of my comment points out, the thing hooks did was make these issues more tractable by reducing the places it was possible to make them. Instead of worrying about which logic is needed between the constructor and over eight lifecycle methods (the number of these was reduced over time of course) for your components, you just have to ask "what does this effect depend on?", "What should it do?", and "how should it clean up after itself?".
This reduces the number of places it's possible for bugs to appear but you can still make them. I believe people blame things on hooks because it forces them to confront those (often extremely subtle and hard to catch until it's too late) bugs earlier by putting them front and center, especially with a lint rule. This is good.
In addition to all that, we also got two major other advantages: sharing of logic/composability and making code using hooks much more minifiable than class components (unlike functions, object properties can't be mangled/shortened by minifiers). Plus we get a vastly improved DX because of fast refresh, which is an excellent bonus.
Agreed. Thinking about lifecycle, side effects and state doesn’t just go away. It’s inherent to the system you are building. Hooks is a way to address that, class lifecycle methods are another. None of these tools will magically let you ignore proper design.
I'd say that most of the time, that's the wrong thing to do. For the vast majority of effects, you don't actually want to call it for every variable that's referenced in it. A good example is literal arrays, functions, and so on, if you have a prop that's an inline function definition, you'll be calling the effect on every render because the reference is new. You could work around this by memoing every inline function definition, but at that point, what are we even talking about. Hooks are a leaky abstraction, and while they do solve many real problems, they come with a lot of heavy baggage once you get into the thick of it.
If this was the case, why do we need to define the dependency list to begin with? If this was both the mainline right thing to do and the compiler can do it, why are we doing it manually? In that case we could just as well omit it by default and only have users define it when they want something different. The answer, in my opinion, is that it's just a patch over what's a leaky abstraction and the added overhead costs more than the benefit in the end.
> In that case we could just as well omit it by default and only have users define it when they want something different.
I believe the official recommendation since hooks were announced is to omit the array unless you need it. It makes things much easier to reason about, and many effects can run on each render just fine. It's mostly[0] a performance optimization. Oftentimes you'll want to make an additional comparison between previous and current props to decide if you actually want to do something anyway.
[0] Possibly entirely. Remember that useEffect's goal is to synchronize with an outside system. Additionally, while I can't find references for a similar thing in the docs for useEffect, for useCallback and useMemo the dependency array is explicitly not supposed to give any semantic guarantees about when the value will be re-memoized. React is free to "forget" values if it wants and re-memoize when needed.
For one, running an effect on every render and running it whenever referenced variables change are two very different behaviors and not universally interchangeable. For two, this is exactly what I mean with leaky abstractions. The fact that you even have to think about this at all is a strong design smell. Vue and co do not have this limitation because they approach reactivity from a fundamentally different angle. They come with their own bag of problems, but I would argue that their smells are considerably smaller than what hooks bring to the table.
> For one, running an effect on every render and running it whenever referenced variables change are two very different behaviors and not universally interchangeable.
Yes, of course. That's why it's recommended as a first step and not as something to do all the time (though you often can if you use something like usePrevious).
> For two, this is exactly what I mean with leaky abstractions. Vue and co do not have this limitation because they approach reactivity from a fundamentally different angle. They come with their own bag of problems, but I would argue that their smells are considerably smaller than what hooks bring to the table.
I'm sure it's possible to do it better, but I'm discussing this in the context of hooks versus the class component APIs they replace.
I'm not very familiar with signals beyond what I've read about them in the past. What I've read about them sound exactly like Knockout observables[0]/computed observables[1], which I'm extremely happy to have left in the past. I assume frameworks using signals must use something to scope subscriptions or enforce update directionality?
Do signals provide a solution to knowing when to run an effect to e.g. establish or close a network connection?
There are a lot of valid criticisms of React, but I don't think this is one of them. These problems are not really new with hooks. They're actually problems which existed in some form in the class component API. Taking them one at a time:
Dependency arrays: I cannot count the number of bugs I encountered which were due to a lifecycle containing some code which was supposed to be called when certain props or bits of state changed, but completely forgot to check one of them.
Stale closures: the second argument to setState allowed this exact bug. Also, people would bind methods in incorrect spots (such as in lifecycle methods) which has the same result.
Misused effects: at varying point, class components had access to the class constructor and the lifecycle methods componentWillMount, componentDidMount, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, componentDidUpdate, componentWillUnmount (this is from memory and is definitely only partially complete). Misuse of these was incredibly common. An article like "You Might Not Need an Effect" but titled "You Might Not Need Lifecycle Methods" or "You Might Not Need the Second Parameter to setState" would have been very helpful in the past.
Hooks reduced the number of opportunities for making mistakes, make enumerating those opportunities easier, and, critically, made them easier to detect and warn users about.