Hacker News new | ask | show | jobs
by jsyang00 629 days ago
You do not need to know how `useState` works to understand how to use it to write a React application, it is fairly intuitive to understand how to apply the pattern.

If I look at your library, it seems to me like it requires a much more complex mental model to begin to use.

Of course, it is better in theory for a developer to thoroughly understand the details of their framework, but empirically, React has been very successful in allowing people to build relatively sophisticated applications while understanding very little of the underlying model.

7 comments

"You do not need to know how `useState` works"

I feel like you eventually do. The issue with React, at least in my experience, is that it's a type of abstraction that seems ill-suited for how the web works under the hood, so it's incredibly leaky. Everything seems to make sense initially, and you get along just fine, but then you run into an edge case, and there's an official workaround for the edge case, but then you run into edge cases for the workaround for the edge case, and suddenly, that's your whole life.

Before you know it, you really do have to know how things actually work under the hood.

I thought pre-hooks react provided a really nice and simple abstraction over how the web works.

FRP is a nice fit for the kind of UI that makes the majority of the web.

Hooks ruined the framework imo. Confusing api (useState returning an array for example).

Having a class component with hooks for lifecycle behavior made perfect sense. Each component’s state being a field on those classes was easy to understand.

Hooks came out of left field and made everything more complex for the worse.

> Hooks ruined the framework imo. Confusing api (useState returning an array for example).

It just returns a tuple. That's not confusing at all and it's a very well established pattern in most modern languages.

If there's anything that can be considered complex and footgun-y in React it's useEffect because most of the time you shouldn't use it at all but it can be abused very easily and it kinda works even if you abuse it (but introduces a huge maintenance burden).

> It just returns a tuple. That's not confusing at all and it's a very well established pattern in most modern languages.

A few things wrong with this. It does not return a tuple, it returns an array with 2 elements. You, as the react dev, need to just know that this is the case and that one is the value and the other is a set function.

In most modern languages, tuples are common, yes. But not in JavaScript. React isn’t a python or go framework, it’s a JavaScript framework. Why doesn’t it act like almost everything else in js land and return an object with 2 named fields? Why place extra knowledge burden on the developer? It’s just poor api design

Yes useEffect is also a huge complex pain point, but it just takes longer to type out why that is.

Pretty much all the hooks are as almost nothing else in the JavaScript ecosystem acts like they do.

> Why doesn’t it act like almost everything else in js land and return an object with 2 named fields

The explanation is actually very simple: because in 100% of times you need to give those two fields custom names. It is easier and more concise with tuple:

  const [text, setText] = ...
Than with object + renaming, which quickly gets clumsy when you have dozens of such lines in your component:

  const {value: text, setValue: setText} = ...
That’s not a good reason.

Especially in our world where strong typing is something preferable.

Fewer characters at the cost of clarity is poor api design. Implicitness is almost always worse than explicitness.

Explicit !== clumsy

And again, almost nothing else in the js ecosystem behaves like this. Especially at the time of the big hooks update to react.

Also, again, it’s not a tuple. It’s an array with 2 elements. JavaScript does not have tuples or the semantics for interacting with them as such. You just need to know that there’s always going to be 2 elements.

That extra knowledge burden is the hallmark of poor api design.

> Yes useEffect is also a huge complex pain point

We have a rule of thumb in our projects: if you use useEffect directly in application code, it is a probable sign that something is wrong with your code, like you're working on a wrong level of abstraction. Almost every direct use of useEffect is better solved with some standard hook from a popular library (like react-use or whatever you like — there are plenty of them).

Like, binding event listeners? useEventListener

Network/async calls? useAsync

timeouts/intervals? useInterval, useTimeout

And etc. etc.

useEffect is really a low-level building block for library code and we rarely use it directly, as it is unsafe and hides the original intent. Much like `new` and `delete` in C++ — you don't use it directly, there are smart pointers. Library hooks are your safe smart pointers over useEffect.

React + classes made sense. Redux was hard to use. All react needed was a simple state management solution that didn't take 500 loc across multiple files and a ton of useless object copying to make a page full of form elements work.

Instead it got a complete paradigm shift to a leaky abstraction. Not that the previous abstraction wasn't leaky, it was alwayz super easy to have components re-render when not necessarily.

There's a world where browser UIs are programmed in Smalltalk instead, which I think is what the authors of the DOM may have intended when building it out OOP-style.

It would bear almost no resemblance to the web we have today, but message passing between UI components throughout various parts of their lifecycle has been a great way to model an interface for decades. You only need to look at the overall fit and finish of a typical MacOS or iOS app, with Swift simply pulling Objective-C into the 21st century.

Web UIs tend to make heavy use of message passing via event listeners.

The major difference between web UIs and desktop apps is two fold. In web UIs, you don’t generally have an event loop in which you change your app state based on system input events and there is, generally, a slow request/response cycle for any interaction.

That’s why immediate mode UI doesn’t really fit well in web land.

Seems like that happens with every frameworks I've ever used since 2000 (in Perl, PHP, Ruby, JS, etc). Every framework makes the easy things slightly easier, the boring stuff is included and you get to focus on the fun/hard part - and I think then, naturally, you bump into the edge. But! You get to that point faster. And then you have to know the guts to solve the issue the "framework" way or do some lower-level shit-hack.

I feel like it's just a natural law of any general purpose framework.

It made the first 80% of the job easy. Now you just have to finish the other 80%.

This hasn't been my experience at all. The implementation details even leak into this crazy thing called "the rules of hooks". It looks like a function but it's actually this new thing called a hook. Which state will you get? That depends on whether the reconciler considers this invocation to be a mount or update. Getting the wrong one? Try restructuring your elements or adding or removing a "key" attribute.

People tolerate this because they learned it but I don't think there is anything essentially simple about it.

If you have an understanding of closures, hooks are quite intuitive.
I have a pretty thorough understanding of both. But I can't understand how you'd find them to be similar in any way. Functions forming closures can be called conditionally, in a loop, or even when no react component is even rendering. Most of the complexity of hooks is not addressed at all by closures, and I don't really what part of their behavior is related at all. Maybe just that you can pass a value to one function, and then get it returned from another one.

The standard hooks delegate to a dispatcher, which has access to the current fiber. The current fiber has a linked list of memoizedState for the current work node. It's true that a lot of the functions that eventually service the hook calls do contain closures. But that doesn't seem to grant much insight into how to use them or how they work.

It's like saying "if you have an understanding of loops, the reconciler is quite intuitive". I mean yes, the reconciler uses loops. But the behavior of reconciliation may still be quite mysterious.

I'll try to fix the comment you're replying to:

I think of React components as not closures but coroutines (and hooks are its yield points).

Hooks are implicitly keyed by index which is the most magic-y/surprising part... but I'm sure if you had to manually key them that'd be criticized too (and would be abused to death-by-bugs, so I can see why they went with this design).

If you understand both points above, you understand hooks.

I still fail to get the (usual) criticism of hooks. They have lots of warts but the API (which is often what gets mentioned) is the most superficial and less annoying criticism. Feels like something that would be mentioned after just skimming the docs. Very shallow.

On the contrary: useEffect is a footgun and deserves criticism. useRef being overloaded to mimic instance variables is confusing for newbies (but this is just a naming issue IMO). The difference between normal/layout/insertion effects is complex and subtle. The new stuff that tries to solve some issues with the concurrent mode (like transitions/deferred value/etc.) feels like a huge hack.

React 19 will come with its own warts (actions, RSC...)

But the API? I don't care at all. Pretty simple, at least for my mental model.

The complaint is they prior to hooks react didn't have any of this complexity. It was pretty simple to understand. Class components works mostly as you expected them to. There are a handful of things that were really hard to do with class components that hooks + function components made easier, but lots of other things became more complicated with each new set of features react has added since then. At this point the solution to "class components are complicate" is far more complicated than class components ever were.
I understand the complaint but my point is that, in my experience (which admittedly might be biased), the complaint usually does not resonate with anyone that did actually use React for at least a moderately complex app.

Hook's API is not perfect but it's a good-enough abstraction that allows the user to have even better abstractions and separation of concerns.

Actual React users did not care about that because the pragmatism far outweighs the theoretical ugliness... which honestly is not even that ugly if you have a mental model similar to coroutines (of course if all you do is OOP a class will look better to you...)

I have recently been fixing some stuff in my old React pre-hooks code and I hated it because class-based components had all sorts of concerns intermixed on their lifecycle methods... no matter how much you tried to abstract them.

Abstracting those into reusable hooks was a breeze and made everything much easier to follow and maintain.

Hooks are far better from a pragmatic point of view.

> There are a handful of things that were really hard to do with class components that hooks + function components made easier, but lots of other things became more complicated

Like what? Does not match my experience at all.

Which coroutine gets resumed by a rendering component is determined by reconciliation. Rendered components themselves have no identity aside from the reconciler's algorithm for equality.

For me, this is the root of the problem. Or one of them anyway.

Hooks are keyed by execution order, but also the reconciler's opinion about component identity, which you can only control indirectly. Fortunately it usually does what you want. Unfortunately it's not all the time.

I feel like I do understand hooks, but it's not from reading the docs, and it's not from using them. I know this is not typical but doing those things didn't seem to illuminate much to me. I would still find them doing things I didn't want that I couldn't explain. It was only after reading the source that I feel like I understand the model. And personally, it's not one I would use by choice. I can do it if I'm required by a team I'm on. But for me, there's a mental overhead for "thinking in react". It's not a natural set of constraints for me.

I think that the clearer way to explain hooks is just referencing how they are implemented.

When a function component is called it is called on a "fiber" a stateful representation of the component instance.

This fiber is available to the hook as if it was global variable that is set before the component is rendered.

This is why you cannot call a hook is a setTimeout or a callback to another component: the global fiber is either unset or has a different value.

The other part is that each hook invocation works on a state accessed as sorta fiber.hooks[index++].

So for example you can call hooks in a loop or in conditionals or in synchronous callbacks, but each rendering must be compatible with the first.

Eg

  let s;
  If(Math.random()<0.5) s=useState({});
  else s=useState([])
Should work.

You could also do the same with useEffect if you keep the length/nullness of the dept array.

Ive probably been doing it wrong, but isn't useEffect needed to make any hooks work? useState does nothing most of the time
No. `useState` returns the value of the state, and a setter. It's not about side effects, it's about the value. The returned setter triggers a re-render. That's not nothing.
Hooks have very little to do with closures.

The key aspect of hooks is that what you are doing is something like this:

function(context) { context.useState() }

Where context is a variable that stores all the hook related data, except in reality that variable is a hidden global variable and useState() accesses that variable internally.

Yeah, they added hooks as global functions so that they look like a "cute" DSL. It saves you the effort to type c.useState instead of useState I guess.

The above code is just for illustrative purposes to get the idea across, according to other commenters the internal implementation has changed from what I remember, but the principle is still the same. Global functions demand global state.

You don't need to understand how useState works if you're writing a page with a button that increments a number when pressed, from a beginner's tutorial.

As soon as you work on any remotely complex codebase, you will run into problems that require a decent mental model of the underlying "magic" to properly understand and solve. "Building sophisticated applications while understanding very little of the underlying model" is how you end up with gigantic piles of unmaintainable spaghetti code full of awful hacks, which seems to be the standard for React applications.

Is this less true of Web Components?

I've worked with a lot of different tech stacks over my career and every single one of them has required understanding the internals once you start using them seriously. I haven't found React to be substantially worse for that than any other tech stack I've used.

With webcomponents you are pretty close to the “metal”. If you know how to write good vanilla JavaScript, you can take most of that knowledge into webcomponents. You only need to learn the custom components lifecycle, and shadowDOM, which is knowledge about web-standards. With other frameworks you need to learn template syntaxing, how state propagates, how the compiler works, etc etc. Lot of that knowledge might be obsolete in 10 years.

Which isn’t to say it can’t be worth it. Learning multiple frameworks and libraries is also very helpful to skill up because you are learning about different concepts and implementations.

Another advantage of web components is the syntax is similar enough to Java (especially with JavaDocs) switching between coding a Java spring backend and a Web component based front end is doesn’t need as much of a mental context switch.
While you do not need to understand how useState, or any other hooks work, you do need to know that this piece of code will behave differently from the rest of your javascript. Painfully, when the calling of (most of the) hooks is concerned, React takes away from the developer the ability to write conditional logic. This is both unintuitive and bonkers, and it requires the developer to come up with convoluted techniques for working around this limitation. This is part of what 'understanding of how hooks work' means.
> If I look at your library, it seems to me like it requires a much more complex mental model to begin to use.

How so?

It has two functions:

(1) createElement(jsx): Allows you to use JSX to write HTML markup. Returns Virtual Nodes.

(2) applyDiff(parent, vNodes): Merges Virtual Nodes created with JSX into the real DOM efficiently.

This is all you need to know. I can keep it simple because I am not doing much in the library. I felt that if I stayed close to the standards, I wouldn't need to do much.

Maybe you have the luxury of users who want applications that have all the reactivity of a DMV form, but in my apps at some point I'll need the very simple priciple of "do something complex when this value changes" and reimplement useState/useEffect in an ad-hoc manner anyways.

I'm more of a backend/embedded developer than a web developer and I still honestly don't get how people find useState/useEffect as intimidating as your comment makes them out to be.

There are a lot of footguns in react if you don't know how use state works.

If you don't know what triggers a re-render you can end up in a state where the data was changed but the UI didn't update, or where the data changed once and re-rendered multiple times.

In my experience the issues are more noticeable with async state changes, like making network requests or state-driven animations.

Aha-ha. Devs constantly try to use dynamic hooks. For example conditionally rendered component which allocates a hook. Yes, React would trigger a warning, that number of hooks was changed. But this implementation detail requires you to have at least a mental model of what hooks actually are to avoid gotchas.