Were React hooks a mistake?
jakelazaroff.com
jakelazaroff.com
React was designed to allow you to think functionally about UI. Your UI should be a pure function of your state. This has always been the goal. All your code runs on every render unless you specifically tell it not to.
Signals are the opposite. A solidJS component runs once, and only the reactive bits ever update. You have to opt into reactivity instead of opting out of it.
The post gives an example of an unintuitive use of hooks, but misses the point. Yes, it's a tricky use case, but the ability to write custom hooks means you need to solve this problem once per codebase at most. The author's "kludgy" solution just needs to be put in a `useTimeout` hook (https://usehooks-ts.com/react-hook/use-timeout). The kludge shouldn't live in the component.
There are some weird cases when you rerender the whole world whenever state changes, but there are plenty of weird cases that it prevents. React's model encourages treating variables immutably and preferring pure functions for modeling your domain. This is hard, because it's not how most JS devs learn to write code before coming to React. But knowing that the whole world is going to rerender gives you tremendous certainty in other areas. Depending on how complex and reactive your UI is, maybe signals would work for you, but if most of what you see on the page is reactive, I'd probably prefer the React model.
Except this has always been BS, and IMHO this fundamental inconsistency at the heart of the model is why the wheel has to keep on turning.
https://blog.metaobject.com/2018/12/uis-are-not-pure-functio...
(Read to the end...)
So if we don't make the incorrect assumption that UIs are unstable (pure functions of model), then we don't have to expend additional and fragile effort to re-create that necessary stability."
I think this could be the key takeaway from the linked article. It hits the nail on the head, on the other hand though, pure functions are _really_ nice, so I believe in the future development of UI frameworks will congregate in that direction, but in a more fine-grained way than React et al.
Yes, and therefore it would be really nice if they were an appropriate model here. But they're not. It would also be really convenient if π were 3, but it's not, and pretending that it is 3 just doesn't give you good circles.
I think you are mixing metaphors here.
An ideal React component was supposed to be a pure function that takes in props and spits out a piece of UI. This ideal has never been reached, or even within reach, because once you introduce component state, you lose the purity of your function — its output no longer depends solely on the input (which is what defines a pure function), but can vary depending on the internal state.
When, however, you say that "your UI should be a pure function of your state" (and when we bracket out the word 'pure' that we addressed in the previous paragraph), then signals are just as good a a way to achieve this as hooks are, or React class components were. You modify your state; some code runs; your UI gets updated to reflect the latest state of your state; voila, the UI becomes a function of the state.
I have seen this expressed elsewhere and and how schwartzworld is using it seems to be the common usage and makes sense to me.
So if my UI, the DOM on a page, should be a pure function of your state then that means when I input the same state to my function I should always get the same DOM on the page.
So if my complete state can be represented by { x: 10, s: 'hi there' } and the resulting DOM can be expressed with nonsensical <div>10 hi there</div> it does not matter what gyrations/changes the state goes through as soon(baring rendering delays or optimizations) as it is { x: 10, s: 'hi there' } then the DOM can be expressed with <div>10 hi there</div>.
> your UI gets updated to reflect the latest state of your state; voila, the UI becomes a function of the state.
I read what you wrote here as expressing the same idea.
That is common now yes and I think that is a good think. Uncommon techniques, now, like storing data in the DOM, uncareful jquery usage, and I am sure other techniques can deny/break a pure functional relationship.
The above is mostly historically why it is important and was not universal. How it ties into what schwartzworld said is that in react you ensure that the UI is a (pure) function of the state by default, no additional work. In signals you have to make sure to opt in every piece of state into reactivity. So I read schwartzworld's argument is that it is easier to keep(?) or at least produce the positive property of keeping the UI a pure function of the state with react than it is with signals because you do not have to do the work of opting in each piece of the state to reactivity.
But most of the production functional React code I see is sprinkled so liberally with complex `useCallback()`, `useMemo()` and `useRef()` calls to prevent unnecessary updates that it seems just as bad as before. Worse, sometimes, because the hooks pattern can encourage more indirection. I still find myself thinking all the time about object references in state, cloning vs. mutating, dependency arrays with 5, 6, 7 distinct dependencies.
Honestly, I don't know if it's any better than it used to be. At least the `componentDidReceiveProps()` logic tended to all be in one place.
But I also don’t understand, why react renders all child components if the parent renders. As long as their props stay the same, they should just stay as they are.
It "renders" all child components if their parent renders in order to check if their props have changed or not.
If the component has not actually changed, it does not actually re-draw it.
With hooks you're still free to manage business logic elsewhere, it just means that you can trivially reuse them across your application without worrying about order-dependence. So I was able to write a hook that returned connection state, and then drove my views off of that reactively.
9 years later my new work involves some react (barely 15% of the job tbh) and I really don't hate it as much.
With React, data flows down from the top of the component hierarchy, always. And it doesn't render until all state changes have been made, so you only render after your data has reached consistency.
I am done with this thread.
Here is a random example[1] I pulled up of someone one thinking MVC is not unidirectional but MVVM is. I am not commenting on the correctness of this impression.
> MVVM is better than MVC/MVP because of its unidirectional data and dependency flow. Dependency is one way,...
[1] https://dev.to/vtsen/mvc-vs-mvp-vs-mvvm-design-patterns-443n....
But what happened instead is that people wrote the controller parts into React components as well. In some codebases there's a very clean separation - 'logic' components that handle and manipulate state, and simpler 'view' components that just take props and render something.
But I haven't yet seen a framework that uses React as just a view layer.
I may be too battle hardened, but here’s a simple rule for React: As soon as you have to reason about execution ordering, you have taken a wrong step and something is wrong.
Your code shouldn’t depend on effect ordering. Trying to do that is like trying to write code that depends on the precise ordering of thread execution in a multi-threaded program. You’re gonna have a bad time.
React can’t win here because they have a hard stance on when to value performance (avoiding re-renders) vs. convenience (ease of abstraction usage).
important typo there.
For an erudite writer it would be painful to hear non-erudite writers blame the pen and paper for the underperforming outcome that they expected to see on their writing. And the cost of correcting that issue in them when modesty is so scarce would make the cost so high an even dangerous, that should be no surprise that only their silence gets in return. Mediocrity posing as great work rejoices.
It also moves so quickly that things don't settle and people have time to get intimate with frameworks and paradigms - I'm talking like 5 year timelines with libraries doing only maintenance updates. Front-end was bad for this because of the browser wars and JS's foundations gave it a really rocky start. The allure of starting over with a new library to get away from the current "bad" way of doing things is often repeated: SPA frameworks over vanilla/jQuery sites, hooks, and now maybe signals?
There are also jobs asking you to be spread very thin among things, while keeping deadlines, so you end up googling around to get it to work, even if it violates some principles of the frameworks you're using. That doesn't really lead to expertise.
Dan Abramov wrote a great explanation a few years ago about an important difference between classes and functions for components, and a particular feature of classes that makes it harder to work with them at scale - prop capturing. https://overreacted.io/how-are-function-components-different... It's worth reading if you think classes are more straightforward.
Plus redux is still maintained. You can still use it if you want to. In my opinion there are better options that plain Redux available now (some based on Redux eg RTK or Redux-Query, and some not eg Jotai, tRPC, react-query). RTK is even recommended by the Redux team.
The fact Dan chose to work on other things is great. He's a talented dev and it's good that other projects benefit from his knowledge.
I'm actually genuinely _not_ sure what you mean here.
For clarity, the timeline was:
- Summer 2015: Dan and Andrew created Redux
- November 2015: Dan got hired at FB to work on React
- Jan-Mar 2016: I got involved writing some Redux docs, and Dan gave me commit rights
- Summer 2016: Dan, who was now busy working on React, gave me and Tim full ownership of the project.
He's had some advisory thoughts and suggestions since then (most notably a couple tweaks to our proposed React-Redux hooks API in early 2019), but has otherwise _not_ been involved with Redux's development since 2016.
Honestly, this has been a _good_ thing. Based on my conversations with Dan, he most likely would have mostly left Redux's design and API as it was in 2016-ish: very minimal, and requiring _lots_ of handwritten boilerplate code to do anything useful.
Instead, I came up with the idea for our Redux Toolkit package that wraps the Redux core and provides a better API for writing standard Redux logic, worked with others to build that, wrote the documentation, shipped it, wrote new tutorials using RTK as the default, designed and shipped React-Redux hooks, and did the work to encourage folks to use the newer Redux APIs. For that matter, React-Redux version 5, which had some major perf improvements, only happened because a user offered to PR the rewrite they'd done for themselves, and I worked to oversee that effort and make sure it covered all the edge cases.
I honestly don't think Dan would have done any of that, both because he would have been splitting his efforts between working on React as his day job and Redux on the side, and also because I don't think he would have felt the need to push Redux forward and improve its usage patterns.
So, the actual history _was_ for the best. Dan got to focus on React, and I was able to pick up Redux and modernize it.
Some further reading for background and history:
- https://blog.isquaredsoftware.com/2019/10/redux-toolkit-1.0/
- https://blog.isquaredsoftware.com/2022/06/presentations-mode...
- https://blog.isquaredsoftware.com/2018/11/react-redux-histor...
That disconnect is why hooks just don't feel right.
As the article states, function closures are how classes are actually implemented in JavaScript so rather than mash the two together, it would seem a more cohesive mental model to simply use a class.
If you step back and look at the broader landscape of UI across desktop (Windows, Mac, Linux), devices (iOS, Android), and web: how many libraries and toolkits are FP and how many are OOP in their abstraction when it comes to managing components and object instances?
There are also a lot of UI frameworks that are completely functional. Take your pick. (ReScript would be the most obvious choice for a react developer)
But for a good reason the hybrid FP/OOP framework React on JS/TS is one of the most popular.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Classes are in fact "special functions", and just as you can define function expressions and function declarations, a class can be defined in two ways: a class expression or a class declaration.
The difference is that React functional components are effectively "parroting" FP and half-in (stateless components) and half-out (hooks). Solid, Preact, Vue are all-out.For me those implementation details are not important. But sure, you’re right.
Even says so in the docs:
https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/
F# has full support for programming with objects in .NET, including classes, interfaces, access modifiers, abstract classes, and so on. For more complicated functional code, such as functions that must be context-aware, objects can easily encapsulate contextual information in ways that functions cannot.
https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref... Classes represent the fundamental description of .NET object types; the class is the primary type concept that supports object-oriented programming in F#.
https://learn.microsoft.com/en-us/dotnet/fsharp/what-is-fsha... F# has full support for objects, which are useful when you need to blend data and functionality.
The underlying CIL representation of a lambda expression is an object; the functional side of .NET (whether C# or F#) is syntactic sugar on top of an OOP runtime.(C#/.NET dev by day dabbling in F#)
Would you agree that C# is not functional?
If you agree that C# is not functional, than F# is also not functional. Nearly every construct that can be expressed in F# can be expressed in C#.
Let’s go at it from the other direction - is Haskell functional? Because it has unsafe pointer access and unsafe IO, so it can pretty much express every C# program as well, right?
Quite the opposite with V8.
The component is still a pure function. It "returns" side effects without executing them, just not as the function return value but through a side channel. But that side channel is sort of a clutch to achieve a pure relation from input to output, just that the output is now a return value from the function plus the sideeffects, which may be executed or not.
That's exactly the point. If you leave an errant function call in a functional component, it's going to affect something other than the output of that component.
My sweet spot for React was when it was class based components mixed with purely functional components.
That combo led to all sorts of "happy accidents" in developer productivity. Logic that needed state tended to be clumped into a relatively small number of class based components, developers actively wanted to avoid adding state to new components because they had to write a lot more code to do it.
So you end up with a structure that looks like several small trees in the code: A single class based component managing the state for several pure function components under it. Overall - this was a fairly happy spot to be. State is contained, performance is usually pretty predictable, pure mappings are encouraged.
Hooks... well - they made state too easy again. Now adding state to a new component isn't a 20 line change (conversion to class). It's just dropping a new "useState()/useEffect()" call in near the top. Very easy. So easy you no longer really think about why that might not be such a great idea.
Now you end up with state creeping through the entire component tree. Renders are hard to predict because any parent component might re-render at any point, you're neck deep into useMemo and useCallback to try to prevent all those extra renders from really killing performance. This is not such a happy place to be.
Can those downsides be avoided? Of course. But developers no longer fall into the right path by default. You have to wrangle them there, and that's a huge downside.
Often with hooks, you'll have to think differently in order to figure out how to accomplish what you want to do more functionally. That up-front cost ends up paying back later because you'll rarely need to touch it, and it's dead-simple to reuse somewhere else. This pattern from the blog post of "fetching state from within a setTimeout" is rare, but your example of accessing state from within a callback should be manageable.
Here's the thing: even though the example from the blog post is strange, the advantage of hooks is that you can pull that pattern out into a hook! So instead of `useState`, you'd call your hook that returns the reactive value, a setter, and a getter that uses the ref under the hood. Then you can use that getter in the `setTimeout`, and you never have to think about this problem again when you have to do this for any other piece of state!
They scale great. Decorators are incredibly underrated for code reuse.
Hooks from the start seemed like a functional mess, everytime I see it I also feel like I’m taking crazy pills.
I'm a back-end dev primarily and never had a good time writing front-end code. Redux just clicked for me. The model made perfect sense! Events from components get handled, and the state is changed. The state is then re-rendered. Beautiful.
It feels like hooks replaced a lot of what redux was doing, yet I don't have the same sense of reasoning as I did with redux.
But again, I'm mostly a back-end dev.
But since react query and swr I think most applications don’t need state management anymore.
If you have complex state, it makes sense. But just to fetch/refetch some queries redux is overkill.
Most redux (and also angular ngrx/ngxs) projects I’ve seen, are just over engineered. They solve an easy task in a very complicated way.
But usually the defaults is what you want. Your user is on a bad connection? Automatic retries are what you get and what the user needs. You user just got back online? React query notices that event and refetches the failed queries.
You need all those features turned off? Just turn them off in the config object.
And mediocre developers are the reality, i don’t think there are many teams, where everyone writes awesome code all the time.
In my experience it's a lot harder to deal with mutable state in some context provider or something.
That's one reason I like redux so much. I expect this functionality from any other state management solutions I encounter. Some others indeed do have this.
Notably the author of re-frame has been weary of hooks.
I think that’s for a good reason. The approach in re frame feels like the best way to manage state so far for a react based app. Everything that changes state flows through an event. State can only be observed through subscriptions. Side effects are isolated to their own type of event. Debugging and testing are so straight forward with these concepts.
Redux got close but it has two problems in my mind. Like hooks, it encapsulates for no good reason. Put the state in one thing that you can observe holistically before and after pure events. It also has too much boiler plate.
I've never seen that before.
I also think there's an issue where react was made to solve Facebook's problems and we're not Facebook.
This is not how I want my grandkids to code software (if the AI overlords let them).
The hooks people came in and said F all that, lets instead create an API that garbles up all the code in one spot, with imperatively nested functions required for more complex use-cases, oh and lets use CONTEXT for state management and just partition items up to deal with re-renders.
They didn't do it on purpose, the react team is highly skilled, but they lacked the wisdom to understand how lower tier or mid-tier developers would use it. I knew plenty of devs that had 3-4 YOE and couldn't grok hooks for years.
Hooks also have some behaviour that makes them harder to understand in certain situations. Closures are quite easy to get wrong, and handling callbacks is quite annoying because they're not stable by default.
I think the timeout example is not ideal, it uses useEffect to handle an event, which is not the way this is supposed to be done and which does lead to various complications. But timeouts are the kind of thing that is much more difficult in hook-based React than it should be. But I don't think this is representative, it's a bit more like a worst case for React for things that should be simple but aren't. You don't write timers all day, you put this in a hook once and write other stuff.
You can still use React the way you used it 10 years ago.
This complaint comes up in every thread about React and it makes no sense.
So yes, this is a very sensible concern that transpired with hooks. Simply allowing reverse compatibility doesn't change the fact that hundreds of workplaces forced people to use a less ideal paradigm.
That isn't React's fault. Your opinionated team members can enforce all kinds of nonsense. This is a general argument against ever introducing new features in any framework or language, and it's not valid.
> So yes, this is a very sensible concern that transpired with hooks. Simply allowing reverse compatibility doesn't change the fact that hundreds of workplaces forced people to use a less ideal paradigm.
"Less ideal paradigm" is an opinion. There are use cases where hooks make perfect sense and are superior to class components.
And again, what your employer and teammates do doesn't implicate React or the React team! They could have enforced Hooks on all components, and dropped support for the old APIs. What should they have done differently? NOT released an optional feature that is loved by millions of their users, just so none of those users can interrupt your ideal workflow? How entitled can you be?
I think components should have been made pure functions, with state/effects being provided by higher order components (HOCs).
For example:
Counter = ({count, setCount}) => {
// pure function here
}
StatefulCounter = withState(Counter, {
count: {
setCount: 0
}
})The 101 parts of functional programming are a very easy pitch. Stateless behavior, testable functions, no surprising side effects from way down the tree. When it gets to 201 stuff - boxing (like useRef) and dispatch (like useState) complexity creeps back in. That complexity is necessary, however, because that's the stuff people actually want to do.
Hooks let react interact with browser audio and video, mapping tools, and other things that are reasons why people want to use web pages. They're complex, but there are limits to how simple a bridge between a functional programming framework and a mostly stateful host system can be.
The emperor gets tricked into buying new extraordinary clothes. They are incredibly fancy, but invisible to anyone "who was unfit for his office, or who was unusually stupid".
The emperor didn't see them, but of course he didn't want to admit that. The whole lot of townspeople didn't see them either, but likewise, no one wanted to admit that.
In the end, the emperor goes to town, naked. Everyone praises his new clothes. Until finally a little boy utters: "But he hasn't got anything on!" and the truth unfolds.
https://andersen.sdu.dk/vaerk/hersholt/TheEmperorsNewClothes...
Perhaps a psychologist could chime in with some probable explanation?
After start the only thing the setTimeout should do is set the game to be finished and then show the final count to the user. If the code wouldn't have used an alert which definitely is a side effect of a function it gets apparent where the code went outside of the React loop. Not defending that but saying that alert and console.log conflict with React's always re-execute approach and for those cases, as you would have guessed, you can use useEffect and only listen to the finished state to execute your alert.
Throwing a console.log/alert into the direct function body of a component just asks to be spammed by rerenders (assuming the average developer)
They provide more or less the same functionality as Rxjs, but are 10 times easier to understand.
Yes, they are not perfect, but they solve a very complex problem. It’s the easiest solution I’ve ever seen.
function createButton() {
var button = document.createElement("button");
function setClicks(clicks) {
button.innerText = "This button has been clicked " + clicks + " times";
}
DB_STATE.get("click", function(err, clicks) {
if(err) alert(err.message);
if(clicks == undefined) clicks = "?";
setClicks(clicks);
});
DB_STATE.on("click", setClicks);
but.addEventListener("click", function() {
DB_STATE.do("click");
});
return button;
}Is that right? If so it's pretty smart I think.
I'm not really a fan of the 'magic' introduced that makes it work via direct DOM manipulation on updates. It seems suspiciously like that's yet another source of mental model confusion that articles will be written about a couple of years from now... but overall it does seem a lot simpler for most usecases.
Where signals get into trouble (I think) is when the structure of the elements change a lot, and that's when I'd expect bugs in DOM manipulation or state handling to show up. You also need to remember to listen to these signals, otherwise you have something that updates fine under certain circumstances and not others.
We're getting there.
Sounds like an X/Y problem to me.
I used both class-based (thank goodness?) and hooks based components but only coz React ecosystem forced me to, class-based were frozen and left in a sorry state - as if React's dictator-in-chief (forgot whoever they are/were, don't want to know - tried to engage once in writing the "beta" React docs but they were horribly toxic in response for no reason) is the only smart person(s) ever and all these OOP language designers are... No, dear Kim Jong(s), you were the ones abusing JS classes in strange counter productive ways and I easily found better OOP patterns for my class-based components. Then I met hooks - the definition of anti-productive. Not to say people don't have a right to write/reflect whatever madness in their code they want to but for me it's a tragedy their "work" became so popular. I don't mean to belittle people who actually have it but hooks is what comes to mind when I think of what PTSD must feel like. I no longer take up JS/TS SPA projects. For the mental strain - the pay feels peanuts compared to native mobile app development so why bother.
What I do know is that the font choice in those code blocks was DEFINITELY a mistake.
I see this a lot, it’s probably the misunderstanding that drives most of the dissatisfaction with hooks.
Having a single instance that exists for the whole lifetime of the mounted component was a major advantage of class components, the major drawback being that you kept losing immutability.
The big advantage of functional components is they give you all that immutability, their drawback is you no longer have a single instance that corresponds to the mounted component’s lifetime, because functions are just executed and then they’re gone (until the next time you execute them).
The way to think about hooks is that React will create a internal virtual instance that does correspond to the lifetime of the mounted component, and it will let you hook into that virtual instance from the function (to store state, effects, refs, etc). The idea was that way, you can have both immutability and single-instance-per-lifetime benefits.
Because React is managing all of these internal virtual instances itself, it can do some nice magic like letting you use a clean top-level “useState” function and it Just Works… because behind the scenes it is pairing up each call to that function with each internal virtual instance (using its order in the call stack, which means the magic fails if hooks are called conditionally, hence the rules of hooks).
That’s fundamentally the concept that lets you work with hooks fearlessly. You no longer had to provision each of your class components with their own instance and make sure they were maintaining it properly, instead your function components could just clock in and clock out and React would take care of making sure they were handed the right instance to work with.
If I had to pinpoint the exact mistake React made with Hooks, it was this: saying you could emulate componentDidMount with useEffect(fn, []). It completely obscures this idea of a virtual instance you’re hooking into. They should have been explicit about the virtual instance model and given developers “useDidMount(fn)” and “useWillUnmount(fn)” hooks, explaining that for each function component you write, React is managing an instance for it behind the scenes and React will make sure it runs the provided mount and unmount functions at the appropriate moments in that instance’s lifetime.
One can imagine a hybrid option, a lightweight class definition (so you could set state in constructor, define DidMount, make refs, etc., in the natural way) that would then return a functional component. That would make the long-lived instance explicit, but I think the ergonomics would unfortunately suck: “render() { return function (props) { return <div/> } }” is hideous, for example.
I get that react is full of problem and many people have to suffer because of it. But we are not even attempting to change status quo here. This thread merely serves as place to rant. and then go back to same problems. At this point, i have most of the opinions memorized and i am not even a React dev.
PS: if people are really into React/vue/angular rants, then have a monthly thread for it.
Technically, React was the mistake. Eliminating React would prevent React hooks from existing in the first place.
There are other approaches like Signals… or Angular… or our totally different approach:
https://qbix.com/platform/guide/tools#designing
There is really nothing wrong with simply having components without unidirectional flow, with being able to simply pass options down to child components.