Hooks Considered Harmful(labs.factorialhr.com) |
Hooks Considered Harmful(labs.factorialhr.com) |
What is a solution?
Otherwise, you can always still use Redux/Rematch or class components (they're still there) or any other state management solution and just pass in props.
If they stop being expressive, I will just start using class components again.
Aka a mixture of them in my code bases.
While I can't vouch for the relevancy of the book nowadays, reading "JavaScript: The Good Parts" when I was starting out myself a) conferred a decent knowledge of the gotchas and b) helped me understand that paying the cost in decreased concision to fence off the dangerous parts of the language (e.g., the abstract "==" comparison operator) was very much worth it. Nowadays, with the abundance of linters and the existence of TypeScript, hopefully the better part of JavaScript programmers get to code in a safer, stricter subset of the language — nevertheless, as the author points out, one can trip anywhere.
That being said, how do any of the above deficiencies constitute an unique indictment of React Hooks, and not, say, that of other UI frameworks or the language in general? React Hooks have introduced neither closures nor shallow comparison to the language. Most of the author's grievances are addressed fairly clearly in their comprehensive official documentation[0] (people still read manuals, right?) or quickly become self-evident through practical usage. Owing to the framework's popularity, linter support for hooks is also extensive, with one's code already being automatically verified against the majority of the documentation's commandments. There's probably not much more than a handful of classes of errors that a developer has to manually watch out for.
I don't mean to say that there's nothing wrong with hooks, but a comparative review that pits them against other frameworks would have been more constructive.
Lastly, the use of the "considered harmful" moniker in the title in spite of the relative scantness of constructive criticism in the article lies somewhere between clickbait, scaremongering and false expertise. It's to be considered harmful[1].
>Hooks benefit from closures because they can “see” and retain information from their scope, for instance, in the example above, user. However, with closure dependencies being implicit, it is impossible to know when to run the side-effect.
The frequency at which an effect is to run is wholly orthogonal to whether the associated function accesses any variables in its environment; it is decided by the developer through setting the dependency array, which is passed as a separate argument to the useEffect function. No relation whatsoever.
[0] e.g., https://reactjs.org/docs/hooks-reference.html#usecallback [1] https://news.ycombinator.com/item?id=9744916
But it’s just the way they did it and how after that there was a complete ghosting on my requests and queries.
1 - https://web.archive.org/web/20190425210338/https://factorial...
When you translate them to Lisp, they either disappear entirely or greatly simplified.
also the post is laser focused on effect hooks which are arguably the most difficult to deal with because of their intended application (reflecting state updates outside of the virtual DOM)
But when I do any react such as assisting another team using it, I am constantly surprised and reminded just how much of a bad developer experience react hooks are.
The coupling of needing to constantly be aware of the intricacies of rendering while also balancing reactivity and data binding leads to probably the most offensive API I’ve ever had to use.
When writing useState or useEffect etc the not always required last argument is a damn array or empty array or sometimes an array with several items.
What does not passing an array mean? Well, I’m sure someone could explain at great length the complexity and alleged need for this but anyway: passing nothing as the last argument of useEffect results in an infinite loop.
Passing an empty array means “don’t render again until actually needed”.
The third option of passing a variable length array results in something else entirely I still don’t understand. Every article does it slightly different.
Ultimately what this leads to for me at least is a developer hostile API full of seemingly intentional foot guns. The other day I had the situation where useEffect to call an API resulted in calling the API endlessly and constantly. Then I thought “ah I have seen this before, it means I have to pass an empty array”.
It’s absolutely infuriating and smells of a bad API design. What happened to the importance of DX (developer experience)?
As I said I’m sure someone could hand wave away the alleged need for this complexity but honestly it simply feels like a bad API. No one will ever convince me that an empty array vs no argument is “functional” because functional code values making side effects and purity deliberate decisions with a focus on clarity.
How do other frameworks let you fetch from an API? Generally a simple assignment and await statement.
Empty array means execute the hook once on initial render.
Non empty array means execute the hook when the value of anything in the dependency array changes.
These are pretty simple to remember imo.
An alternative design could have been to pass in an object as a second parameter instead of an array with these options details like
{ runOnlyOnInitalRender: true }
Or { alwaysExecuteOnRerender: true }
Etc. But that feels far more verbose than the three options we have now.But why should we? No other reactive framework makes the developer go through this ritual. Even RxJS is clearer to understand.
> Etc. But that feels far more verbose than the three options we have now.
A better designed API would require neither.
I consider your teammates harmful. All this stuff is mentioned in the excellent React documentation.
> The mechanisms to retain memory have a lot in common. Classes use this, which refers to the object’s instance, while functions implement closures - the ability to remember all the variables within their scope.
Non-arrow functions, like the example, defined with the `function` keyword also have `this` context. If you want to limit `this` and rely solely on closures, you probably want an arrow function.
When you query data, it is cached locally in a shared data store that will automatically trigger re-renders of components that take in the data types as a dependency.
You don’t have to write an update to the data, you query it and Relay updates all the underlying models in the local state cache.
The data types are correlated to your components via GraphQL fragments which allows Relay to know what and when to update.
Because most of the values you are using in your component are already stateful, you end up using A LOT less hooks since most state updates will trigger and happen for you.
It’s incredibly powerful once you get going with it.
The downside is getting it to work properly with the type generation. It can be rather finicky and Facebook likes to rewrite the API out from under you every couple years.
> Most bugs can be solved by moving hooks away from the components
Well you can't modify state from outside the component, and the context becomes harder less clear... to me hooks are still the best option.
That is absolutely not the only difference.
I think hooks solved some problems at the cost of introducing others, such as this. It's unclear to me still whether the value trade-off has been worth it.
No... what? Could not disagree more with this sentiment; this is the "for the love of the game" kind of stuff that completely loses focus on why we write code in the first place; to make money. Very, very few people have the resources to care about this level of problem, and far more often people who don't have the resources end up spending them on useless "improvements" like what's discussed in the article, rather than building things that users can use (or that make it easier to build things users can use).
Hooks are surely flawed, in the same sense that literally everything in software is flawed. The point is not to select a way of writing code that doesn't have flaws; the point is to select a way of writing code that has flaws you can live with.
Hooks have flaws, but hooks also have benefits that make writing software meaningfully easier. Losing sight of that is a great way to write an article that complains about problems that never impact the user's experience.
It's just such a big red flag when someone talks about "incorrect code" that doesn't impact the user in any way. Huge, gigantic waving red flag.
Of course when you change some of the code without changing other parts that rely on the original code, it will break. I'm not sure why that's unique to hooks.
useEffect(() => {
console.log(`hello ${user.name}`)
}, [user.name])
There OP, fixed that for you.Object.is(..) || deepEquals (...)
Perhaps should I ditch hooks for Rust?
> I do not exaggerate when I claim that I find a dozen of hooks-related problems every single week while reviewing code.
I also see sooo many issues when reviewing code like leaked event listeners, unstable props causing re-renders, etc. And these issues show up from teammates who otherwise write impeccable and trustworthy PRs in other regards.
I enjoy writing hooks style code, and for me reasoning about lexical scope & closures is second nature. But for many engineers used to OOP, hooks code is the first time they’re asked to do this kind of reasoning since leaving university. In OO Java/JavaScript, it’s very normal to declare a new class and have the only two scopes be the current function’s locals, and class instance members. Hooks code on the other hand can easily reach two or three layers of local closure scopes, each with different lifetimes. I think this is fun and clever, but I also prefer to maintain boring and simple code… I’m worried that hooks ramps up trivial complexity too much in exchange for often-unneeded power.
On the other hand, function components and hooks tend to guide people more toward splitting up their big mega components into smaller parts. At all companies I’ve worked for, product eng across the stack tends to produce mega objects cause it’s easy to just keep adding private methods to do one more thing, and splitting up responsibility of state encapsulation takes some extra reasoning. At least with FC/hooks, the API and linter force you to unroll funky logic or loops into sub-components, since you can’t call hook in loop or conditional.
I've been in and out of the React world for the last 5 years, and this statement hit hard for me. Between the major shifts in best practices, the abstractions on top abstractions and constantly tripping up on the slight syntax differences between JSX and markup, so many commercial React apps I've worked on make me want to pull my hair out, and not just because of hooks.
In hindsight, the virtual DOM hype has not lived up to expectations, and I find newer frameworks like Svelte to be so much easier to work with. With the amount of React code running today, it's hard to see a future without it, but I'm so ready for it to be supplanted by something simpler.
And that's while writing a lot more React than I did Vue.
I think this is true. However, I don't think I saw less bugs with `component(Did|Will)(Mount|Update|ReceiveProps)`. Lifecycle events are intrinsically about state management, and that has always been the root cause of many bugs. This isn't a React specific problem either. Back in AngularJS 1.2.x days, the $scope and digest cycle was the source of many bugs. In Backbone, people's two-way binding between views and models were the source of many bugs.
Are React hooks complex? Yes. I don't think they're worse than what existed before though.
With hooks, we traded verbosity for a single interface that does it all (assuming you know how to hook up your dependencies correctly, or compose helper hooks to manage state comparisons). Hooks allow you to do mostly anything lifecycle methods did, but they're a lot trickier to reason with, review, and develop.
This all goes away if all your developers are functional maestros - in practice, it's lead to buggier code across our various frontends.
I’m curious if the author sees dozens of problems every week in code review due to, say, unexpected null/undefined values, mistyping variable or attribute names, using the wrong number of equal signs, failing to catch errors and handle rejected promises, etc.
unexpected null/undefined values
This is an issue and has led to bugs. Switching to TS and making the compiler flag them fixed it.
mistyping variable or attribute names
Not an issue. Code obviously breaks if you have incorrect names.
using the wrong number of equal signs
This is an issue but causes few bugs. Linting generally catches it, though cute boolean punning still bites us.
failing to catch errors and handle rejected promises, etc.
This is an issue and has led to bugs.
Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
Most common I see:
1: Missing dependencies (for folks who don't use the linting rule)
2: Not understanding reacts async state model: example being expecting state to have been updated immediately after calling setState inside an effect
3: Not understanding closures: an example is creating an interval in an effect that uses and updates a useState value, but being confused why it isn't updated when the interval repeatedly fires.
I agree wholeheartedly with the author. Hooks are powerful and even I was super excited to start using them when they released. But now I think the hooks paradigm leads to even worse and bug-ridden code than what we had before.
The docs are pretty good, but don't seem to cover very well that when using hooks you really need to know JS well, which includes equality in JS, closures, etc otherwise you will be guaranteed to shoot yourself in the foot.
There is so much more business logic mixed in with component code at seemingly random in projects as well such that it makes finding where "stuff" happens even more difficult than before.
"there is room to improve documentation blabla"
I have not dealt with many junior devs, but creating an object either by using a literal or restructuring is still creating a new object and no one should expect it to be the same. Equal perhaps, but not the same. Maybe someone should explain scope and instance lifetime in the JavaScript world instead of blaming React for these. Because there is no surprise that the component re-render when you change the prop.
Hooks are a constant area of struggle and people make tons of mistakes (forgetting to cleanup a useEffect, useState closures, and needless useCallback are the top 3) with it.
I dare say that if we didn't have MobX things would have been much much worse.
The annoying bit is that other frameworks like Vue or Solid have MobX like Reactivity baked in which makes things much much simpler.
I think the part of the reason React has been so successful is because its rules are easily communicated; components which render HTML or composed other components, state lives inside those components and props are passed down between them. How to deconstruct an interface into individual pieces and how data should flow through them really resonated with a lot of people.
But I think the shift to hooks means we’ve have lost the clear rules that made React so accessible to newcomers. Although hooks are still easy to get started with they seem to create confusion easily, and one wrong dependency or deriving state incorrectly and your laptop becomes a heater. This makes it more difficult for developers to focus on what they’re meant to be building because their heads are filled with an uncertain palette of distributed logic.
Now that the tiny API and rapid learning curve seem to have been abandoned I’m starting to think React may no longer designed to help solve the problems me and my teams are being asked to solve and perhaps the reason why hooks aren’t clicking for many people isn’t because they aren’t smart or willing enough, it’s because the mental model required to use React effectively no longer overlaps enough with the things we’re usually building.
- Relying on `useMemo` preserved object identity assumes a semantic guarantee, which React docs tell us explicitly not to do [1]. Not providing this guarantee is ridiculous. If their cache is implemented correctly, this should be no problem.
- The alternative is to leverage an external lib, which does provide this guarantee [2]. However, it's weird that bringing in an external lib as the more "correct" solution to this incredibly common problem (this is seriously relevant to like 1/2 the components I write)
- Wrapping every bit of derived state in a `useMemo` hook is incredibly verbose and annoying, especially when you take dependency arrays into account. I feel like I'm writing generated code.
Class based components are fairly straightforward. The entire render function is run on every change.
Solid/Svelte are fairly straightforward. The component is run once and then only the reactive parts change.
Hooks run the function on every change but there are islands of non-running code inside the constantly rendering body where only run when their dependencies change. This strikes me as an obviously intermediate solution and I don't want to spend time porting/developing something that's going to be obsoleted in a year or two.
The next framework generation is underway and the time for early adopters to move on is in the next year or so. The main reason to hold off is that the handling of SSR/hydration/etc is in flux and I believe the primary benefit of the upcoming generation is going to be ability to avoid shipping most component code over the wire.
I have a lot of respect/love for React. I've seen quite a bit of criticism that the vdom idea is inherently inefficient but the important thing about it was that it was reliable. Lots of JS used to do all sorts of crazy stuff with the dom underneath you but React has basically cleared the field of most of that which allows more fine grained approaches to work consistently.
The 12 months of hell that followed cannot be described in a simple hn comment. The dangers of any technology that confuses code reviewers about scope is one of a small handful of lessons that have stuck in my brain, almost a decade after I left engineering behind.
Speaking for myself, I’ve only used them a few times, but I’ve found footguns in even very simple usage. Not months of hell, but definitely propositionalToleranceForHell < actualHell. And sure maybe that’s lack of experience with hooks specifically, but they have nearly-totally-whitespace-and-punctuation-diff equivalents which are nowhere near as complex to actually use effectively.
There is no need to worry about dependencies since it won’t need to run again! Hooks like useState() do not emit a value — they emit a “signal”, a function that returns the current value.
This article introduced me to the concept: https://typeofnan.dev/solid-js-feels-like-what-i-always-want...
Unfortunately there’s no react-native equivalent and the ecosystem is much smaller, but I have to imagine the React team has their eye on this alternative strategy.
The difference between observer approaches like SolidJS or MobX (and I'd also put Svelte in this box) and React's data-flow centric one is one of explicitness. With the observer approach change tracking is more implicit, i.e. embedded into the values you are using and the functions that are using it. Which does fix the problem of forgetting to declare dependencies, because using == declaring.
Now what it does not guard you against *per se* is unnecessary re-runs, I am willing to bet there are tricky cases with tracking of nested objects and updates based on partial changes there. SolidJS does expose various tools for untracking and batching signals, so it might be a matter of trading initial explicitness/complexity for adding it later.
This might be the right trade-off and untracking might be the smaller problem. But that feels like a somewhat team and product specific question. I think it is clear that without state libs React is to barebone to handle most somewhat-complex interactive apps, and a lot of them are observer based (MobX, Recoil, ...). But I do not see it as a silver bullet just yet.
Maybe this could be solved by a hooks "standard library" that provides generally useful hooks like useTimeout and a useMemo that is actually stable as mentioned in @purplerabbit's comment.
- Hooks are tricky because you need to pass them an array of dependencies, which is manual housekeeping
- You shouldn't pass anything but primitives to a hook's dependency array if at all possible
What is the alternative? Just pay attention to the two above, or go back to class based components? Or will there be a React-flavored JS/TS (like JSX / TSX) that has different closure mechanics?
Does building a rich GUI experience on a web application need to be this complicated?
I still remember the days when rendering are mostly done on server side and Javascript was used as progressive enhancements. The web application back then were quite interactive and building them were somewhat simpler than the current state of the art.
Too many people think ‘functional programming’ is “not OO” but there is also that bit about “no side effects” and hooks are all about side effects.
The reality is closer that, state should be immutable, and minimized as much as possible, but at the end of the day, almost every interesting problem requires storing state. Once you reach that point, classes are simply a better solution for state than closures. Especially if your class and its variables are immutable, you get all the benefits I mentioned and none of these tradeoffs. Your state is explicitly stated.
Sorry for not adding more. But just so this. I’ve loved learning Elixir of late. But my programs need state. I really miss how well I could model general state using objects like those afforded by Smalltalk and Python. I wish there was a best of both worlds, but I wonder if to do one well, you have to overreach so much with the one philosophy, that the other just can’t be tolerated well in its presence.
I've found that it's really, really difficult to design good UI, without state. The "solution" that many UI systems use, is leaving the state in the view, but that often results in a pretty degraded user experience. Sometimes, the state needs to live in the model, as it may interact with a whole gaggle of views, or apply sets of state.
So the "solution" there, is to tie the views together, or save the state in little "statelets," connected to views; resulting in an ... interesting ... design.
I've come to learn that "hard and fast" rules are a mistake.
It's been my experience, that I often need to approach a solution in a hybrid manner, and really appreciate new techniques and technologies.
But sometimes, we need to stick with the classics.
Whenever you need to have warnings and rules for using things (that require linter verification to make sure developers aren't shooting themselves in the foot with common/regular usage) it's an anti-pattern.
They are far too easy to mess up, especially for something that is meant to be a fundamental part of the library.
Only the React community actually. It's the majority, but I wouldn't equate it with the whole frontend community.
I remember when they were introduced - it was during the brief moment(1 year) during which I worked in React.
My code-smell-o-meter indicated that this is going to cause problems in the long run.
Ultimately it did, since you can't just replace everything with hooks and call it a day, as that is likely to cause massive performance issues.
You can tell me react is a big brained functional library all you want. Fact is you're putting business logic and mutable state inside your functions from props -> jsx. The fact that setState is a 'hook' doesn't change the fact you're setting state.
Every react code base I've come across looks exactly like what they told us not to do in the WinForms and Java Swing days - code behind.
The other issues mentioned are JavaScript specific (not React/hooks specific), currently there are libraries that can help (or using a compile to JS language that has proper equality semantics). In the future there might be an implementation of the proposed immutable "Record" and "Tuple" types that will have data literal syntax and all the properties that one wants when writing FRP style UI code.
I like React a lot. A simple conceptual model, like elm or other frameworks mentioned here, would work better that this constant change.
(defmacro while (condition &rest body)
`(let ((cond-fun (lambda () ,condition))
(body-fun (lambda () ,@body))
(while-macro-run-time-function cond-fun body-fun)))
Then we have a run-time support function: (defun while-macro-run-time-function (cond-function body-function)
(loop while (funcall cond-function)
do (funcall body-function)))
Closures allow macros to parcel off expressions or bodies of expressions into functions, so that control structures can then be made "remote": put into a function.This has the benefit of keeping expansions small. Another benefit is that since the core logic is in the run-time function(s), those can be updated to fix something without having to recompile the macro invocations.
Somehow, the sky doesn't fall in Lisp land; we don't need articles like, OMG I learned about this in 2018 and it's so dangerous.
I see a difference with hooks, where you need a linter to verify that you used them as intended: they must start with useSomething and be called in the top level. As opposed to use native language features.
Hooks work with function components, and are how component state and the equivalent of lifecycle methods are implemented in function components.
For a while with function components, higher-order components were a common complementary approach to hooks, which have themselves largely had their function subsumed by hooks, so you could maybe describe the trend as:
class components => function components + hooks + HOC => function components + hooks.
I wish old methods would be deprecated and all features made available in function components.
> Most bugs can be solved by moving hooks away from the components and using primitives as the only dependencies.
Or just use primitives in the dependency array of your existing hooks?
There are good reasons for _not_ doing this, since using these hooks isn't free; and technically speaking useMemo isn't an identity guarantee (though it currently behaves as one), but I haven't experienced any of the common useEffect pitfalls since adopting this methodology a couple of years ago.
But as my sibling comment points out, a lot of the need for this defensive coding would go away if there were more ways of defining equivalence. I hope that one day the record and tuple proposals land, which should help a bit. But i'd also like to see something like Python's __eq__ and __hash__ in JavaScript too - perhaps done in a similar way to [Symbol.iterator].
Perhaps it is a concern in bigger codebase. AFAIK, there is no method to enforce this kind of contract. But documentation could be a big help. Like documenting how changes to a prop impact the behavior of the component - like the common `initialValue` and `value`.
It took me a long time of grinding on various effects scenarios to figure out efficient, easy to understand solutions to complex behaviors. That said, I do agree with his points on under/over subscription... that is still something that frustrates me, especially when the linter wants me to complicate something that seems unnecessary.
I prefer frameworks like e.g. Svelte that works with what JS affords. If you want to go full FP Elm or ClojureScript will give you more value for your money than React imo.
To me, I still think Hooks are useful for making that big swing attempt in JS despite needing so many crutches like hard education spikes in the learning curve and so many linters, but the problem with Hooks is definitely not that they are "FP" but that they are advanced FP that even FP still hasn't figured out all the bugs.
From /u/wycats "The rule in Octane is: "mark any reactive property as @tracked, and use normal JavaScript for derived properties". That's pretty cool!"
[also a quick shill for the general-purpose language I'm working on which internalizes that mechanism at the language level :)]
We use Relay to define our state management, trigger global updates across many components, and also query/mutate our data.
Does MobX have a way to do the global state management that Relay does?
I’m curious because after using Relay, I’ve found most other types of state management to feel extremely heavy with boiler plate code.
Thoughts?
[0] Technically, it wraps all the different data structures in Proxies etc as soon as they get assigned into an observable structure. But the goal is for you to never have to think about them as anything other than plain data, and that abstraction very rarely leaks.
I've created a wrapper library which is basically a more-opinionated, higher-level version of Mobx if anyone is interested: https://www.npmjs.com/package/r2v
I’ve looked at Mobx and am concerned about mutation. I would like a magically reactive state container that always returns immutable views of the state for local usage. Maybe I should stop worrying and switch everything to Mobx.
One, you don’t rely on a semantic guarantee if you use useMemo for derived state. Avoiding rerendering counts as an optimization as far as the React docs are concerned (your program works if there’s an extra render), and this is in fact exactly what it was intended for. The docs you linked seem to agree: Regardless of whether an offscreen component keeps or doesn’t keep its useMemo, the code is correct and there’s at most one extra render.
Second, while I agree with the verbosity complaint, I personally make a point to use useMemo as coarsely as possible. It’s often completely fine to compute all derived state in a single big lambda that returns one big (immediately destructured) object. It’s only when you have multiple pieces of derived state that update individually and are also all expensive to compute that you actually need fine-grained useMemo calls. And in this case, you can always think about extracting sone of that logic into a helper function/hook.
It’s not perfect, but I think it’s possible to avoid a lot of the pain most of the time.
Example: if you're not using a smart intermediate layer like `react-query`, you can unintentionally trigger loading states and re-fetches if you're not closely watching dependency array identities
This is correct, but it’s exposed specifically for cases where you know more than the compiler does. This set of cases will almost always be smaller than similar cases in React, because ultimately Solid components are just functions. They don’t have a lifecycle, components themselves never rerun unless you call them.
Those cases for React: you have to tell it when it should rerun or not rerun your components, your logic etc. Basically everything on the event loop that isn’t already participating in its reconciliation algorithm, and everything which has its own diffing logic.
Those cases for Solid: likely interacting with other libraries with implicit lifecycles. Your event handlers will all run exactly as defined. You’re already invoking the signals and other logic which reconciles Solid’s state model. You just need to really mean it when you “get”, and libraries which aren’t aware of that need a little nudge.
Was there anything wrong with class components? It's what I learned half a decade ago, and the idea of a "state" object made so much sense. Now, with hooks and whatnot, it seems like React is trying to be "functional" without actually being so.
The idea, in a very rough nutshell, is to allow separating behavior and presentation.
Hooks are the reusable unit of behavior. You compose hooks into more complex hooks that might implement loading and saving data from/to the server, for example.
Then you can use this hook with different components, or use the same component in a different context with a different data source. This can be very powerful if used well.
But as I said at the beginning, hooks are unfortunately also very difficult to get right.
Class components when needing more complex behaviours related to lifecycles/context had the choice of:
a. Hard wiring up all the different lifecycle events to bespoke systems, on a per component basis (use same thing in 5 places? Implement it 5 times)
b. export const ActualWorkingComponent = HOC(HOC(HOC(HOC(NonWorkingWithoutHocComponent,{conf4}),{conf1}),{conf2}),{conf3});
I will fully admit that useState is more aimed at singular/simple values, so a more complex object is a pain to directly copy over. (useReducer or wrapping setState could work, as the child components shouldn't re-render unless their props change).
But it is so much nicer to have the dependencies at the start of the the functional component, and not in a horrid callstack at the bottom, with 3 different ways of defining which prop gets which HOC's values/functions and then injecting even more callbacks to munge those values from the out HOCs to use in that HOC to make it's output good for next HOC...
Instead, it's just:
const {widgetId} = useRouterParams();
const widgetData = useSelector(selectWidgetById(widgetId));
No mess, no fuss, use it it in a few places? Wrap it up in a function of it's own.1. State you read in a ~hook~ aka Composable is/should be an instance of a special kind of observable object. You can create your own subclasses or just use the standard state container much like useState. This means the system has more information to produce minimum subscriptions/reactions at runtime.
2. The system used crazy compiler transforms to turn functions marked @Composable into reactive scoped hooks/components. Using the compiler eliminates a lot of error-prone boilerplate and bookkeeping code otherwise required for these kinds of systems in standard OO languages without monad+do-notation by adding a sublanguage “manually”.
Downside to the Compose model is that it’s even more mindbending to understand. Developers are encouraged to surrender to the magic. I’ve yet to read/write enough Compose code to understand the cost benefit analysis yet.
React may have limited options with this design, but other frameworks have taken other approaches to the problem:
Vue/Svelte/MobX only run the setup code for hooks (or closest equivalent) once. Derived values and effects are automatically run without specifying dependencies - the tools detect what an effect reads while it runs, and track dependencies for you. Since effects are only set up once, closure values from the setup scope don't expire/disappear, so they can't go stale in the same way as in React (caveat destructuring). I think Solid is in this camp too, but I haven't used it.
Frameworks like Mithril and Forgo ditch state tracking and effects entirely. You explicitly tell the framework when to rerender etc., and everything else is just you calling functions and assigning variables without the framework's supervision.
Crank.js extends the explicit-rerender idea by using generators for components. This preserves the "a component is just a function" feature from React, but avoids the hooks pitfalls by only executing a function once.
Hyperapp doesn't have the notion of components at all, so you can't have component-local state. The framework reruns all view code at once, passing the current global state. You can approximate components by writing functions that slice and dice state and divide up the work, but that's transparent to the framework, and there's no place to store state besides the global state.
These all have trade-offs. They may require more complex runtimes / toolchains, or simply shift around the burden on the programmer (what's easy/hard, what kind of bugs will be common).
I'd love to see more approaches in this space. Not all trade-offs are right for all situations, and I'd like to see more ideas that meaningfully change the development experience, rather than "if you squint it's basically the same thing" ideas.
Correct. Solid is all about signals (reactive values). When you run any effect (rendering updates are effects created behind the scenes for you), it will get run once immediately, tracking which signals where called. Then it will subscribe to those signals to re-run the effect on change, and it will resubscribe to newly called signals, and unsubscribe from no-longer called signals.
I believe that it is roughly equivalent to Vue's reactive api, except that rather than using a proxy or setters to allow object mutation to trigger effects or re-render, it uses separate update functions, more like react hooks do.
(I argue that ESLint is almost required when working with React. You can turn off its weirder other rules if they become an annoyance, but the hook rules are golden.)
No, I think the problem is the combination of closures, mutability and identity. Very few other things in programming punish you that harshly (and subtly) if you don’t have a crystal clear understanding of all three concepts.
Writing rich UIs with progressive enhancement without this benefit was over-complicated pain before React. All of your UI code would either be based around binding to static HTML generated by the server or be based on locally-generated elements; if you ever had a widget that had been generated by the server that you now want to generate client-side in some context, or vice-versa, then you had to have multiple separate code paths on the client for that in addition to the separate server-side code for the widget. Having the component's code defined once in a way that works for all three contexts (client-side generation, server-side generation, client-side binding to server-generated HTML) is great.
I think people assume that because React is a newer way of doing things that it doesn't work well at the old goals (progressive enhancement) but the opposite is true!
If you're trying to write applications in a web browser (putting aside all arguments about if we should) then you need to care about rendering performance. And that means dealing with the underlying problem, which is correctly invalidating and regenerating the minimal subgraph of a big dependency graph. And the framework people are constantly trying to find the best way to present the inherent complexity of this problem in a good way, without too much additional complexity.
I've had several conversations where fans of Hooks will justify them by saying that "functional programming is about composition over inheritance".
And I think that's entirely missing the point of functional programming. The goal wasn't to remove inheritance in favor of composition, it was to remove STATE - which in turn results in the nice property that functions can be composed, because they take all relevant data as arguments (they are pure).
Hooks basically blow that away - you've added back in all the problems of local state, but now you've hidden it behind a brand new paradigm that developers just don't have a very good feel for (even years after the introduction of hooks).
I'm reasonably well-versed with hooks, and even I find myself having to do incredibly complicated and deep dives into upstream code to answer simple questions, like "How many times will this hook run?" or "How many render cycles will this hook introduce?".
Sometimes the answer is so far upstream it's basically impossible to answer without running code - Ex: if you depend on the "useLocation" hook from react-router-dom, and you pass the entire object as a dep to useEffect (which is a mistake in and of itself), you will be fine in the browser, but Jest tests will trigger an infinite render cycle, because JSDom generates a new object for each call of window.location.
I can reason about functions that are pure, and that's the freaking point of functional programming. I cannot reason about functions with hooks in them - it's FAR worse than class based components in basically every way except ease of re-use.
I think in many respects - we threw out the baby with the bathwater.
I came here to see this said...regardless of the method used, state is what is challenging to maintain, regardless of how your framework or tool modifies and tracks it. And the only way I know of to properly wrap some sense of sanity around complex state modification is with unit tests, again, regardless of framework/tool. If you can't test it with a unit test, then you're going to struggle manually testing it as well, even if it does usually work.
A side-note is that I always thought the obvious split for functional/class-based React components was stateless/stateful (as full-blown objects are basically purpose-built for tracking state), so I was surprised when I joined this new project at my employer and learned about the interesting world of hooks. I rarely dabble in React however.
My snarky side today wants to add "developers struggle with maintaining state, what else is new".
In Haskell for example you can put all of your code in the IO monad and just have side effects anywhere. This works fine. But you quickly realize that there are benefits to separating out code with side effects from code without. The types make this clear. Haskell provides powerful mechanisms to weave functions that both have side effects and those that don't with ease while maintaining that clear separation.
If anything FP in this manner is an extremely powerful version of side effects. It's not about "no side effects at all" but rather taking control of them and using them to our advantage.
This belies your whole previous argument...
Everyone understands that side effects are a requirement (literally - a program with no side effects is useless). Functional programming herds the programmer into a situation where code that creates side effects is consolidated into just a few places, and the majority of the code is pure functions.
That paradigm has a real cost - consolidating side-effects isn't particularly easy, and you have to work to do it.
But in exchange you get a LOT of pure functions that are
1. Easy to reason about
2. Easy to compose (because they have no side effects)
3. Easy to test
Hooks are the antithesis of this - they create code them seems pure, and has the guile of being composable & testable, but in reality they are very hard to reason about. They have completely undone the work of consolidating state and side-effects into one location. It is very easy to call a function with a hook in it in a way that breaks that function, and it's usually hard to reason about what subtle differences are causing this new breakage.
Next has amazing docs and sane defaults: it's the only way I would touch react.
But, sorry : what does that have anything to do with hooks ?
The problems that this comment described are exactly where I shot myself in the foot with hooks: https://news.ycombinator.com/item?id=30754873
I don't recommend react in general but it should be pretty straightforward even with little react experience.
This comment [1] describes exactly how I shot myself in the foot w/ hooks. In my head this is how React talks to me now: "Tell me your dependencies. NO NOT THAT KIND OF DEPENDECY!!"
In order to work, the formula has to follow Dijkstra's admonition about abstraction: "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." The implementation of Y can be opaque to users, but it has to be precisely defined on the level that users think about it.
Hooks were not presented this way. There was never a precise definition of hooks presented to users, at least not one that I could find in a succinct form in the documentation. To me, the documentation amounted to a handful of examples and a couple of rules to follow, and you were supposed to pattern-match your way to success, without any precise definition to fall back on when you were uncertain.
Hooks are the kind of complex technical idea, which some developers (a certain Mr Abramov) find interesting.
However "complicated and interesting for navel-gazers" are normally rather negative characteristics in tools. Tools which are "reasonably simple, predictable, easy to reason about and easy to compose" are far more productive.
It may have changed since then, but hooks were basically just weird shitty methods & properties with silly non-standard declaration syntax and totally bizarre access/invocation rules, in a language that already had fairly normal objects & methods and all that.
I haven't built anything with SolidJS, but it's not my style based on the docs. It is interesting that you bemoan being so far from "regular" code in Svelte when it's basically a minimal superset of HTML, as opposed to Solid, which uses JSX (which feels very far from "regular code" to me). Also given the fact that Svelte is a compiler that emits vanilla JS, I have a lot of more control over performance, compile-time checks for things like a11y, unused deps, etc. I'd argue that svelte source code looks a whole lot more like it's output than SolidJS does, but that't just me.
It's easily the best overall front end developer experience I've seen, but I've only built smaller projects with it so far.
Personally I prefer Compose because it’s open-source (so you can figure out how it works if you need to), has much better docs, and seems less welded to FRP stuff which I don’t enjoy. I don’t really have enough experience to really review either though.
> This is an issue and has led to bugs.
> Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
This is something that can be mitigated via the no-floating-promises linter rule if you're using TypeScript[0]. For the cases where you actually want to just swallow errors, you can just add a `.catch(noop)`. This makes such situations explicit. You can get even stricter with the no-misued-promises rule[1].
[0]: https://github.com/typescript-eslint/typescript-eslint/blob/...
[1]: https://github.com/typescript-eslint/typescript-eslint/blob/...
touching on an immortal truth!
People would also constantly get tripped up over `this.props` and `this.state` when it came to computed state values. Now a simple `useMemo` simplifies and expresses that intent way better than setting something conditionally in the constructor and doing a bunch of conditional checks on componentWillUpdate before calling this.setState again.
Edit:
Oh, and the improvements with Redux are life-changing. The `useTypedSelector` UX is so much better than writing a mapDispatchToProps, mapStateToProps, and then having a bunch of merging ceremony there.
And yes, the fact that the hooks work so much better with TS than `connect` was one of the major reasons why we now recommend the hooks API as the default.
You'll see some similarities with the haskell libraries auto and netwire.
I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did. It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
The FP term of art I was forgetting yesterday was: algebraic effects. Hooks as a whole are trying to model algebraic effects. Algebraic effects are still raw and new in a lot of FP languages. They are hard to reason with even in research languages designed entirely for testing algebraic effects work. The one time I saw an example of trying to model algebraic effects in Haskell it used a half-dozen or more GHC extensions including GADTs on top of monads. (GADTs applied to normal types create higher-kinded types. "Higher kinded monads" was a bit metaphorical, but not the worst description I could have come up with for how algebraic effects looked to me how I saw them modeled in Haskell that one time months ago.)
I wish what React was trying to do was just dumbed down Arrows or Observables. From what I understand of how things like React Concurrency and React Suspense are built to work they are doing a lot more reasoning under the hood about the hook effects than just the raw arrows would imply. I think it is an interesting big swing. I also realize that using ideas from way out in Research Land in a non-type-safe language is a huge risk and liable to create a lot of footguns (as it has).
> I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did. It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
I have mixed feelings on this. Pure JS has some advantages. While hooks as they exist offer some footguns, they also allow for some flexibility when you need it (sometimes, very rarely, you do need a closure around a local variable that is not intentionally in the dependency graph). Admittedly right now a lot more people are likely to succumb to footguns than need the flexibility. I think some of that is a balancing act that hooks and especially useEffect are very low level "assembly language" in React and the impression I have is that they mostly were never entirely meant to be used to the extent of writing say 100% of one's business logic in these low level hooks and instead were always intended to be lego building blocks in "higher level" hooks. (Related in part to how in React you aren't likely to build entire components' VDOM by hand in JSON notation, you'd probably use JSX or TSX.) Right now the React community maybe isn't using or building enough "higher level" hooks because the low level hooks also seem to have created a decent "good enough" local maxima hill that "current wisdom" says to die on that hill ("just use hooks, you don't need redux") rather than search for a better mountain.
I think the Typescript wrappers in the article here are helpful. I think the article's reminder here is a useful one that just because you could do everything with raw useEffect doesn't mean you have to and that there are a number of good state management and service layer libraries that are still very useful in React post-hooks.
Don't forget too that trying to do everything in raw useEffect code may be a sign of putting too much business logic in your views and abstracting that out can be a good separation of concerns no matter how you decide to abstract that service layer (and/or which tools like react-query/Redux/Mobx/etc you choose to make that easier).
Nothing stopping you from keeping the prior response while loading the new one to handle loading states.
- What about anything computed from the previously fetched data? Will it be computed the same way?
- What about any user-provided state downstream? Will it be preserved? Will it still be valid?
- What about any user-provided state midstream? Even if preserved, will it evaluate the same way after a refetch?
- If you know mid-/downstream user input might be impacted, can you detect that and ensure each case has a desirable outcome, or does this responsibility spread to all of those cases?
- What about inconsistent network connectivity? Will it fall back to the previous state in case of timeouts? Is it even supposed to? (Is the request idempotent? Do you know? Can you know? If it’s not idempotent, will it recover after a timeout once network available resumes?)
- What happens if user/event/timer-caused state changes while the request is in progress? How will computations be reconciled?
- What happens if network-provided data is also supplied by user input from other users? Do you have a reconciliation strategy?
- What happens if this first request triggers N requests? What happens if each of those N requests similarly has to answer all of the above questions?
- What happens if any one of these has a pathological case which causes it to cycle? What if it causes a cycle intermittently?
- What if your user is using the cheapest mobile available and has an expensive data plan?
- What if everything is really fast, actually, and your user has motion sensitivity?
I’m just rattling instinctive thoughts after stumbling on this comment. There are surely more I could come up with if I were actually dealing with concrete problems where unexpected redundant network requests are being evaluated as “is it more than a performance issue?”
[1] https://twitter.com/dan_abramov/status/1104414469629898754
Crazy how many choices there are in the mix (use-deep-compare-effect, the `JSON.stringify` approach mentioned by Dan, `useMemo`, and `useMemoOne`). Feels like a "pick your poison" scenario, as each one has a significant issue.
That being said, `useDeepCompareEffect` does seem the most "foolproof", and "foolproof" is probably more important than intuitive or performant in most cases.
That's why I brought up Relay (this is not the same as React, but works with React), cause it's what FB uses to stitch together global state between hundreds of different sub-components.
That being said, Relay does NOT have a simple object interactive state ability. It's an obscure "external store" with weird querying and mutating rules that can feel quite difficult to work with at times.
So if you’re working on multiple tiny changing projects, or with contractors who are only gonna be around for 6-12 months, it’s possibly not worth it.
But if you’re working on a project that needs to stick around, and the people you’re working with are colleagues who will remain in the company even if not on the same project, the training is totally worth it, IMO.
I disagree. The presence of a hook is the indicator that something impure is happening. Seeing a hook should be equivalent to seeing a promise, option, IO type etc.
Hooks also compose beautifully together. You can make so many great new hooks by combining just useState and useEffect together, bundling up that functionality into a new hook that you can then use in any UI.
Yes. And that's my whole point.
React was very powerful when care was taken to place impure code into a single class based component, that then passes state down to pure components as props.
React is a lot less powerful when developers scatter hooks everywhere.
New developers no longer have to go out of their way to understand the render lifecycles of a class based components, and feel the pain of writing componentDidMount or componentWillMount or componentWillUnmount or shouldComponentUpdate functions. Instead they just throw a hook in. Which is mostly ok - but it's hiding that you do actually still have to care about how this whole shindig works (and opens up a whole new world of pain around identity and equality checking, re-render cycles, dependency passing, etc)
I'm not saying hooks don't have an upside (ex: I'm right there with you, I mostly prefer a hook to an HoC from a reusability stand point) but hooks let developers shove their head into the sand and mostly pretend that they're writing a pure function - and they're ABSOLUTELY NOT.
You absolutely should not be scattering hooks everywhere in your code base. The same principle applies to use them higher in the hierarchy and pass down props.
This is a simple principle that can be taught to a new React developer. Keep your state at the highest level it makes sense to no matter the state mechanism used.
Hooks allow for composition of effects in a way that class based components did not.
Even when immutable data is easy and is good from a software design perspective it is often a terrible choice from a performance perspective. Advocates say the performance loss is just a factor of two in many cases but that's why FORTRAN survived so long against C, why people are developing Rust when Java is available, etc.
There's a reason no on is writing modern games in functional languages, and that reason is performance.
But that said - At least for me - the major attraction of React was that it really concentrated on making ui related code pure. Give a component the same props, and you get the same DOM.
That's a really powerful concept for reducing bugs, easing testing, and giving you composable components.
It is not a performance improvement.
I think hooks really hollowed out the value proposition here. Because class based components were more painful, I used to see a lot of care and thought put into consolidating the logic that generated props into a single class based component (consolidating state). That component would then mostly pass down props to pure components.
Hooks make it easy to just throw state into any old component - which is nice in some sense, but like I said - it hollows out the value proposition of having pure components.
Good teams will still try to write mostly pure components, but many folks will just liberally scatter hooks into their code, creating code that becomes increasingly hard to reason about.
The ecosystem matured around C-style procedural language concepts because naive functional implementations simply weren't fast enough (and were often much more difficult to work with).
Yes - some companies do leverage FP concepts for development, but they're usually heavily modified for that specific purpose (ex: GOAL at naughty dog, ECS for Unity)
And even then... ECS is "vaguely" functional at best. The entities are mutable, and the logic in the systems is directly modifying those mutable entities. I appreciate that the logic is applied consistently, and I think there's value there that comes from FP - but it's very much not classic FP.
There was though - it's the same pain you're referring to later... "Hooks allow for composition of effects in a way that class based components did not."
Class based components sucked in a lot of ways. But the nice side effect of that was that folks tended to use them more carefully, and avoid using them when they didn't understand them (or at least avoid implementing any method besides render()).
I'm not saying hooks don't have nice properties - I'm saying that I'm not convinced (after using hooks for about 2 years now) that the price you pay is worth it.
The number one source of bugs in our codebase is... drumroll... hooks. I think a part of that is that state in general is evil, and will be where most of the bugs lurk. But I think the other side is that hooks have a completely new, unintuitive, hard to reason about set of rules. Composable? Sure, sometimes, if you work really hard to understand exactly what sort of new rules you're creating and then hiding in their complexity. Intuitive? Fuck no!
It sounds like your org could use some simple guiding principles and code reviews. You seem experienced, this shouldn't be a big problem. Maybe help guide your junior devs?
There's power there, and I absolutely agree that hooks do a better job of making for re-usable code than HoCs, I just think that the general level of understanding for them is low, and most devs do a really poor job reasoning about them (and in generally - I find they're basically impossible to reason about in isolation).
I see people do things like wrap everything in useMemo and useCallback, or pass complex objects to useEffect as deps, or fail to understand that making the output of useState the dependency of a useEffect hook that happens to call the corresponding setState function is a recipe for lockups, or any number of other fairly simple mistakes.
Plus... tools like redux strongly encourage destructuring semantics, and destructuring for hooks is absolutely the wrong thing (for the same reason - equality and identity checks). But then you're in a conversation about object identity and memory locations with a dev who has never encountered a pointer in their life, who's 6 months out of a bootcamp, and whose eyes are glazing over further and further with every word out of your mouth.
Worse - hooks can give you a loaded gun if you expect all the environments your code runs in to act like a browser (see my useLocation example with JSDom). Works a-ok when tested in a browser. Will even work nicely for the specific tests you might write for your component (since folks generally mock their hooks) but will absolutely foot-gun you if another spec calls the real hook. Happens to eat up a boatload of CI cpu usage and time as well.