> 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 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.
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.
I have to say, I've never felt this in the Vue ecosystem. It's relative simplicity in state management, and slower relative paradigm shifts have been pleasant. The 2->3 shift wasn't perfectly done (and it did split the state management stories), but overall I've felt like I "kept up" fairly easily, compared to React.
And that's while writing a lot more React than I did Vue.
There’s too much framework in Svelte for me; I don’t like feeling so estranged from “regular” code. I think SolidJS is more appealing - although it might be even more clever than React…
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.
Interesting - I began using Svelte after years of using React, and I found it to be far less framework-y. I feel much more connected to what's actually happening, but I also haven't built out a very complex application with it yet. Is it the weird $: reactive model that you don't like? That was the weirdest part for me, even if it's supposed to be "normal" javascript.
Svelte feels very minimal to me. It is regular JS where I can say "shove the value of this variable onto the web page and rerender that component when the value changes" but saying all of that is just $:
I had bad feelings about React when I first saw it. Then I saw RiotJS and that just clicked for me. I like Svelte too - haven't put it in production tho
> 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 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.
I don't entirely agree - the old React way was verbose and explicit about when events are executed. This allowed developers to more concretely reason about logical workflows in applications.
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.
My experience has been pretty different. With lifecycle methods, you had the implementation details of a single concern split across the constructor, multiple lifecycle handlers, and sometimes even the render function when refs were involved. Hooks can express full concerns in a reusable way. This is a valuable abstraction that previously required complex higher order components to do.
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.
I'd chalk it up to a difference of opinion then - I like simplicity, but I'd take verbose clarity over it any day. Having to explain to newer engineers the nuances of hooks (and the permutations required to wrangle them in) is harder for me than saying "this exact lifecycle method is what you're looking for, take a look at its documentation".
Chalk it up, then. I've never had a problem explaining the nuances of hooks, and there isn't actually that many permutations needed to wrangle them in indeed - but our engineers are usually quite quick to pick these things up and understand the interface without having to worry about the implementation anyway.
It is a challenge for many developers. It’s taking developers I work with between 2-5 months to really grok it.
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.
Thank you, glad to hear that our React-Redux hooks are useful!
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.
> I do not exaggerate when I claim that I find a dozen of hooks-related problems every single week while reviewing code.
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.
I'll give you my answers to your questions as someone writing production JS/TS. Answering your questions is precisely illustrative of why hooks issues are hard to catch.
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.
>> 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.
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].
Thanks for those references, I've recently been handed an overwhelmingly "JavaScript" project and this'll be a great way to expose some overlooked code paths.
Don't have a blog post, but the majority of issues you will find are with dependencies arrays for useMemo/useCallback/useEffect.
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.
The article here ("Hooks Considered Harmful") actually probably would be better called "Most Common Mistakes When Using Hooks". It seems to cover the most common ones I'm aware of.
After dealing with years of the hoc stacking hell, hooks makes the logic sharing problem so much easier. It's like switched from callbacks to async/await. I am not sure if there are better solutions.
I'd said that it is a non-issue. One of the main tenets in React is that your component re-renders when the prop changes. I think anyone should research what exactly change in this context means and take care of not triggering the re-render when it's not wanted. The author goes out of the way to use examples that are what I would consider bad code and not something that should ever pass a code review.
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.
The point is that "a prop changed" can't be reasoned within a component. If I receive an object as a prop, I can only reason if that pointer in memory will be the same, not wether it has changed. The allocation can happen up in the component hierarchy making this a subtle for even the most senior developers.
That's why a number of people (myself included) advocate for overzealous/defensive use of useMemo/useCallback as a means of ensuring that a prop changing is always meaningful. The rationale is summarised here: https://www.zhenghao.io/posts/memo-or-not
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].
Props are outside the boundary of concerns for a component as far as allocation go. You react to value and identity. The parent component is the one concerned with actually giving you the correct props. Very much like a function call would work. You can provide a signature or a contract, but you are not actually expected to deal with every kind of abuses, especially things that fall outside good practices.
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`.
In an ideal world, you're absolutely right. Reality is different, though—sometimes people make mistakes and generate objects on-the-fly without memoizing them, and sometimes you have to deal with third-party or legacy code that you can't control. Reasoning about the behavior of hooks in these types of situations can become really difficult, and there really is no good answer aside from being ultra-defensive with how you handle props and dependencies (which has many of its own downsides).
in java terms, it sounds like you need to define your own "equals()/hashCode()". The "reference equality vs logical equality" is well-understood in that domain at least.
This was my overall thought as well. When I get myself into the type of trouble he's describing, it's usually because I either designed the effect poorly or it's simply doing too much. I also find that I'm abstracting certain complex effects into Redux Sagas more and more which solves some issues around effects depending on the result of other effects- not all processes should be triggered in this way.
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.
Relying on the distinction between reference and value equality here is clearly not a good practice - as a fix, the author recommends only using primitive types as dependencies.
I work in an infra team in a large codebase for Microsoft. We are the people who do the sort of bug-analysis to figure out what technologies work well and what needs improvements.
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.
For me it seems React has made some unfortunate choices in an attempt to go FP in a language that does not afford it very well. Instead of embracing JS and working in alignment with its limitations/offerings, the React team has introduced elaborate workarounds that gives a whiff of FP but introduces indirection and verbosity and does not solve basic problems of state (i.e. the example from the blog post with classes vs. closures).
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.
FP is a wide spectrum and JS can do some FP well. I'd argue the issue with what React is attempting with hooks is not that it is FP, but it is an attempt at particular "higher-kinded" monads when JS today mostly just has basic support for simple kinded monads (Promises/thenables and async/await syntax). The types of monads that hooks want to be are advanced and require a bunch of extensions even in "big FP" languages like Haskell. (In theory, if the biggest concern was no do-notation in JS, you could approximate it with async/await syntax. The mental model of async/await might not make sense to what you were doing with it, but the dualism with do-notation should be valid. What Hooks are trying to be needs things like GADTs and other extensions in Haskell from my understanding.)
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.
Not sure what you mean by higher kinded monads but hooks are kind of like dumbed down Arrows.
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.
I was curious and indeed confirmed that ember.js/glimmer.js @tracked properties were inspired by MobX.
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!"
One of the great things about MobX is that it works on plain data and plain operations on that data [0], not just the contents of a sanctioned Store, which means it has no opinions about where you put your data. Local objects captured by closures? Cool. Classes? Great. Mutating arguments? Go for it. Global singleton? Have fun.
[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.
It is! It can be overkill for very simple React apps, but I used it to build a power-user tool a couple years ago that would have been impossible to do with Redux or hooks.
Hooks are never the solution for state on their own. They're more for localized to a single component state.
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.
I only wish Mobx's API was terser and more opinionated. Hard to derive a good Mobx pattern from their bloated API (>100 exports)
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
If Solid or Svelte had a way to import React components (for 3rd party code) and a documented migration path for 1st party code I'd be interested in switching.
How does Mobx help your code work better? Does it specifically salve hooks wounds, or just because you don’t need to manually manage subscriptions in a component?
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.
Because you don't need to manage subscriptions, there's a whole class of hooks you don't need to write in the first place. Then on top of that, you get extremely good performance. Having to rarely think about hooks and basically never think about performance, for me, free's up tons of cognitive space to focus on the actual implementation of the UI. Its not a perfect system, but I haven't found another I'd rather use.
I was a very early adopter of React and I am very grateful for it. It’s now it’s my job to listen carefully to the concerns of my teams and I’ve helped developers of all abilities and experiences through their troubles on all sorts projects over the years. In that time I’m sure I must have discussed all the good and bad things that can be said about React but if I’m honest I’m not hearing as much good nowadays.
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.
You can get around this identity problem by creating all derived/composed objects via `useMemo`. This ensures that their identity only changes when that of their dependencies do. This lets you get around this "identity problem", but comes with some issues:
- 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.
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.
I'm with you on thought #2. Regarding your first thought, however: if you want control over when `useEffect` callbacks fire, identity isn't just an optimization, it's a necessity. For example (used in another comment): 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
I'm curious if you've used useDeepCompareEffect, the use-deep-compare-effect npm package? I've found that it is pretty reasonable foolproofing for many of these identity questions. I'm well aware of Dan Abramov's objections to the deep equality checking [1] but I still find it a bit easier for me and other devs to reason about when doing things like data fetching.
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.
Oh, that’s a good example. I’ll argue that you should try to use primitives (strings/numbers) as keys in those cases. But if you can’t, then you’re right that identity is critically important.
Identity is necessary if you want to predictably trigger `useEffect` callbacks.
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
Though this is also a strong reminder that though useEffect can entirely replace service layers and state management layers such as react-query/Redux/Mobx/Relay/what-have-you doesn't mean it necessarily should. (Ultimately that's the bottom line summary from this article.) useEffect is a very "raw" low-level tool, at some point it is a good idea to consider a higher-level tool (maybe one based on useEffect under the hood).
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).
Disclaimer: I am working on a project which does not use hooks (or React or anything like that), but has a fairly complex set of data processing specifications. The project is > 10 years old, the project is a product in the sense that it has direct end users, but a library/framework in the sense that its behavior is also defined by end users (it’s not a UI around arbitrary spreadsheets, but that’s a pretty good common frame of reference). Most of these questions are informed directly by work I’m actively doing, some by past work on distributed systems and UI/UX.
- 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?”
I have not adopted hooks and have argued against it for others on the team. My primary objection is that the paradigm is not clean.
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.
17 years ago, a colleague convinced the team to rewrite a Java server application using Aspect Oriented Programming. It was a new and magical technology that would allow us to separate the meaningful business logic from annoying logging and bookkeeping - at the slight cost of losing track of standard OO control flow and scoping.
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.
Judging by some of their actual expressed feelings about using hooks, there is definitely a subset of React devs who are definitely experiencing months or more of hell. The React team is, admirably, actively working on a next version of docs to help address this. The fact that the docs project is hooks-first is both a testament to how radically React itself has changed over recent years, but also at least a smell that hooks themselves may be inherently a pain point.
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.
Hook are worse than lifecycle methods, they are difficult to understand, and they are not elegant. When you run into trouble, it is your mental model to blame. You are told to learn to think in hooks, which is to say, to do things in an unintuitive way. That is a heavy price to pay for composability.
Kind of neat how SolidJS solved these issues: each component function and hook runs only once.
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.
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.
> 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 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.
The main problem is that the hooks provided by React by default are low-level primitives of an incremental computing DSL, and you need to have a very good understanding of both JavaScript and the semantics of this DSL to work with those primitives correctly; it feels like having to reimplement strcat every time-- with every pitfall that you might fall into while reimplementing strcat in C.
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.
No way, hooks are great. All of these problems exist in Class components too -- especially accidentally rebuilding `params` in `mapStateToProps` or whatever. Figuring out how to do things on re-renders with componentDidMount and componentDidUpdate is a total disaster. Don't even get me started on getDerivedStateFromProps. God, hooks are better in every way.
Alright so, reading through all this I think I can summarize it:
- 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?
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.
I’ll prefix by saying I agree with a lot of the criticisms, and I’ve experienced first hand how hard to explain and error prone hooks are. But for this post, let me defend the concept.
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.
To expand on this, many of the things that hooks now make easy were bespoke per-class implementations with details that leaked into every lifecycle hook. Think of how you’d write a chain of useMemo calls in a class component to see just how bad it was.
State wasn't evil, the lifecycle callbacks were. I don't think they ever deprecated setState, but I'm pretty sure they've nudged people away from the lifecycles..
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...
With class components, lifecycle management is trickier to encapsulate and reuse since you have to split such things up and scatter them across several different methods or wrap the whole component in a HOC, and in general class components require more boilerplate. Thus you'll usually end up with code that's just a bit more spaghetty than with function components. With hooks, you can at least encapsulate some specific behavior behind exactly one function call without jumping through any extra hoops.
I also liked/like the idea of one state object. With hooks now they really discourage packing everything into one state object and you either have to use a bunch of different `useState` statements for your different variables or pack a big state object in one `useState` but then you face big performance issues with needless re-renders when you only change one element of the state object.
I would say mixing state and behaviour was the main problem with class components (and oop), although it might be a better problem to have than all the approaches supported simultaneously (some code with class components, some hook heavy, etc)
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.
Is SwiftUI pretty much the same in this regard? I’ve only taken time to do the trivial examples in either Compose or SwiftUI, but they feel very similar, so I’m wondering if your prognosis also applies to SwiftUI.
I think the Swift compiler does less magic for SwiftUI. They added a new literal syntax for the UI view tree literals, but other than that I believe the behavior is in the runtime. SwiftUI expects you to use Combine, Apple’s (F)RP system, to a greater extent than Compose expects you to use RxJava/Kotlin Flow. My impression is that Compose is more React-y and is actually an escape from RxJava FRP-land; I can translate most hooks to Compose after reading the Compose docs a few times but find it much harder to do the same with SwiftUI/Combine.
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.
> What is the alternative? Just pay attention to the two above, or go back to class based components?
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.
> I think Solid is in this camp too, but I haven't used it.
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.
It’s often not even that much manual housekeeping. If you follow the second advice, then ESLint will do a fine job of telling you exactly what’s missing or superfluous.
(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.
ESLint is fantastic, but what would be more golden is unit tests failing or code not compiling. It is almost like hooks need to be written in a meta language like JSX so that they can be compiled and thus failed when written poorly.
I think most specifically the call out in the article is: the Hooks that React provides are extremely "low-level" (and intentionally so) and while you can do everything with just raw low-level hooks, consider returning to higher level hooks. The article provides some simple (Typescript type-safe examples) of higher level hooks. It also recommends that Redux/MobX/Relay/etc are still very useful higher level tools, even or especially in hook-based components in React.
Unless I am missing something, functional components and centralized state (a la elm https://guide.elm-lang.org/architecture/) are enough. Bringing global state (Context called from anywhere) can make it less clear.
This is just a casual observation from a back end/infrastructure guy perspective.
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.
You don't need something like React for building rich GUI today but once you have a sufficiently large codebase and a large team of developers, you're going to end up building inferior in-house versions of libraries/frameworks like React/Redux just to have any sane structure in maintaining the application as well as support future feature development. The currents state of front-end is complicated but I absolutely prefer it over the chaos of old timey vanilla javascript.
React is a great way to do progressive enhancement: you can have the same component code run on the server and client. On the server, it will only be used for creating static html. On the client, the component code will bind to the already generated HTML and allow client-side interactivity.
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!
It doesn't need to be this complicated. Unless you want to efficiently re-render the minimal amount when something changes, which you probably do if you have a big UI and want your users to like using it.
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.
A tiny dom lib like https://github.com/WebReflection/uhtml is more than enough for very complicated UI, with understanding how events work, will be able to implement very thin state management on top. With game programming styled manual render() call here and there as needed, pretty neat.
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.
> 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).
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".
The entire "no side effect" aspect of functional programming is just a huge misunderstanding at best. It's unfortunate so many pushed that narrative. Many FP languages do not even restrict side effects. But those who do, like Haskell, do so in order to communicate where those side effects are taking place.
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.
> But you quickly realize that there are benefits to separating out code with side effects from code without.
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.
> Hooks are the antithesis of this - they create code them seems pure
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.
> The presence of a hook is the indicator that something impure is happening.
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.
There was nothing preventing you from scattering state everywhere in class based components. On top of this the component tree became a huge mess of HoC's stacked on top of each other.
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.
> There was nothing preventing you from scattering state everywhere in class based components.
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!
We can just agree to disagree then. I'd find libraries all the time on github which had class components using state in weird ways you wouldn't expect.
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?
Eh - I'm not really sure we're even disagreeing. I just think that hooks let you stack the abstraction tower a lot higher, and answering some fairly basic questions can become really hard.
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.
Some things are easy to express as functions (compilers). Other things aren't (user interfaces).
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.
That reason is not performance but familiarity and ecosystem.
A trendy way in gaming to build games is to use ECS which is FP and there are very performant framework to do ECS.
I would argue (pretty hard) that the reason is actually performance.
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.
FP has also benefited from a hype cycle, and FP does improve code in a lot of ways. Making your data immutable makes it easier to reason about, and pure functions prevent surprises. However if your argument is state is bad, and FP avoids state, so that is why it is good, but you encounter a scenario in which state is required, then the benefits of FP start to degrade.
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.
Bugs caused by faulty equivalency-related code have been common for a long, long time in React development. I couldn't guess how many bugs related to object destructuring I wrote, and fixed, long before hooks existed (although I can confidently say the former number will be significantly higher than the latter). I used to see these all the time on large Redux/React codebases. `mapStateToProps` was always a fun place to find them.
I want people to be critical of the method du jour, but these 'production-like code examples' defy most of the modern conventions that a React dev would use. For example the fetch example seems to ignore its own prior destructuring assignment to make an awkward reference to an object property.
I agree, but the problem is that when you refer an object inside a hook, the eslint exhaustive deps will tell you to add that object as a dependency. In that example, it's easily solved with destructuring `const teamId = params`, but a lot of developers will blindly follow the eslint complaint.
> The argument usually follows that state is evil, hence object-orientation must be avoided.
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.
I agree. Hooks are an awful hack / offer poor user (developer) experience imho.
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.
I think people need to wake up to the fact that react encourages bad architecture.
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.
I am building my first non-trivial Next.js app now. It definitely took a couple days to get a simple "fetch data from a third party and render" use case working. And even now I'm not sure if I'm holding it right.
I had a Next.js project due for an interview and it basically took my entire weekend, even with a working prototype in another framework. The interviewer later on told me that the assignment was his way of experimenting to find out if the company should switch stacks. I told him it was the worst experience I've ever had coding.
I was on Google's Web DevRel team for 6 years. Owned all the Chrome DevTools and Lighthouse docs for 4 years and led content strategy for https://web.dev and https://developer.chrome.com for 2 years. Lots of experience building small applications in vanilla HTML/CSS/JS and I build toy apps in whatever frameworks are currently popular. I'm a technical writer by trade so I usually don't have time / motivation / business rationale to dive into a particular framework to the point of mastery but definitely am not a novice either!
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!!"
I'm not a fan of hooks, really -- it feels like a big black box. I tried to find the source code, but I wasn't able to grok how it all actually worked. Maybe just me, but hooks are a bit too magic-ey for my tastes.
For this reason I was skeptical of them from the start. It's a broken formula: X is challenging for some developers to understand, so we'll replace it with Y which practically nobody will understand, but it's okay because they won't need to.
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.
I think these insights capture very clearly why "hooks" are a problem rather than a solution.
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.
I read it when they first announced it. There was a lot of indirection, which is probably why you had trouble, but in the end, hooks were registered to a list that was associated to your component object (yes, object) in the big, central state machine that is React.
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 really used React much since hooks were introduced (not because of hooks, just a coincidence). Up until that point, I had been using React almost daily. At the time, I remember thinking "What is this for? What problem does this solve in my already well-architected and organized front-end?"
On the object identity issue - I always wished JS had some way of getting at a unique (printable!) identifier for objects. It’s not very often but sometimes it would be so very useful to be able to console.log(obj.underlyingPointer) to see if an object’s identity is changing. It would also be useful from a teaching perspective.
useEffect is a bit tricky but powerful. The issues mentioned in the article are things you can avoid by reading the official docs a bit carefully and playing around for a few minutes.
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.
IMO hooks are a DSL implemented in js. React went from class to function components to hooks as preferred practices. In the process, the previous was not cleaned up (until very recently, maybe still, you needed a class component to catch exceptions in a component). It feels like change for changes sake, were only new features are explored without making the whole consistent.
I like React a lot. A simple conceptual model, like elm or other frameworks mentioned here, would work better that this constant change.
(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 think probably you intended to reply to some other user, unless I am missing sth.
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.
> React went from class to function components to hooks as preferred practices
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.
That sounds right. In my own anecdotal experience I see a much bigger push to use hooks for everything, where before only useEffect and a few others would be expected.
I wish old methods would be deprecated and all features made available in function components.
No, "state" doesn't mean "keeping things around while I compute other things". That's just intermediate results. State is defined by its ability to change as the computation runs; and the changes can affect how the computation unfolds. That's why it's "evil": uncontrolled state changes are difficult to reason about and are great cover for bugs.
considered by you. for me they're the best way to write react components, and i was never confused as to how they work.
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)
I agree with this article, when I’m doing frontend development I much prefer to use Vue - with its options API (I ignore the composition api as it’s essentially a clone of reacts hook system).
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.
I've been using SolidJS for some personal project and they use most of React concept but avoid pitfalls by running once the code of each component. Also, no need to explicit dependencies! The framework figures it out automatically. Really nice dev experience.
> 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.
We use a thing called Relay (https://relay.dev/) to manage our querying, mutating, and state management.
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.
> It is then safe to say that the only difference between programming paradigms is how long you keep stuff around and the space-time tradeoffs that these decisions entail.
> This decision forces the programmer to be responsible for making explicit those implicit dependencies, thereby functioning as a “human compiler” of some sort. Declaring dependencies is manual boilerplate work and error-prone, like C memory management.
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.
In the final example there is a create effect and an unsafe create effect: could these be collapsed such that something checks for any argument that makes the call unsafe and defer to the unsafe version but otherwise chooses the safe version? Seems the split still relies on the human to know which one they need to use without that extra bit of logic.
> Most of those issues never manifest to the end-user, but incorrect code that is not a bug today will, eventually.
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.
Maybe I expressed it incorrectly. When the code is incorrect, it fails in the future. So the user will notice it. This happens when your code relies on some context to work (for instance, if it relies on how the data structure was allocated somewhere up the component hierarchy). When the context changes, then that piece of code starts failing, therefore impacting users.
But now it just sounds like you're saying, "When the code changes, the code changes." which feels obvious?
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.
In our codebase, we use useEffect without exhaustive-deps linting rule. I don't think it is bad in hands of developers who have basic understanding of closures in javascript. Otherwise, we have to resort to complicated logic to provide same functionality like keep tracking of changed variables. Are we wrong to do that?
The main problem with hooks is that it takes a lot of somersaults to do simple things. Just like class components, there are things at the core that are not kept simple. which makes hooks very complex.
Maybe this author doesn’t understand the history of the term “considered harmful”? The points are valid, and there’s lots else to be wary of in hooks, but the title shows a lack of experience.
Read docs, implement linters. It doesn't take much time to learn the dependencies and nuances of useEffect (a single afternoon of reading the docs).
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.
I respect the opinion that JavaScript can be an unintuitive language to write in. The concurrent existence of abstract and strict equality operators, the former's lack of predictability, as well as their performing shallow comparison are all a fount of problems.
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.
I know this might be a bit off topic but I wanted to mention it: FactorialHR had an forever free plan[1] which they killed and forced me to go onto a paid plan. This happened after I recommended them to a lot of founders which then backfired on me (albeit a bit - ppl still trust my opinion). Needless to say all those startups migrated to a different HR provider cuz they just couldn’t trust a company like that with sensitive data.
But it’s just the way they did it and how after that there was a complete ghosting on my requests and queries.
> 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.