>Conceptually, a component seems better represented by an object/class than a procedure/function.
In other paradigms, it is! Our paradigm is exploring the functional take. I agree it's a bit unorthodox but we are very intentional about modeling it that way. It really has a bunch of powerful properties one might not expect.
>And aren't classes just really functions under the hood anyways?
The key difference is that in React, UI is a pure projection of current data (props/state). You're always supposed to "return" the UI. Sure a class is a function, but that function is invoked once. Its methods can be called many times, but having a pure render() method (like in class-based React) is really a class cosplaying as a function. Functions are more honest to what React is trying to be.
> Classes may seem like the ideal thing to hold state since that's what they're designed for. However, React is more written like a declarative function that keeps getting executed over and over to simulate it being reactive. Those two things have an impedence mismatch and that keeps leaking when we think of these as classes.
>Another issue is that classes in JS merge both methods and values on the same namespace. This makes it very hard to make optimizations because sometimes methods behave like static methods and sometimes behave like values that contain functions. The Hooks pattern encourages the use of more statically resolvable calls for helper functions.
>In classes, each method has its own scope. It causes issues like us having to reinvent default props so that we can create a single shared resolved object across those. You also encourage sharing data between those methods using mutable fields on the class since the only shared thing is this. This is also problematic for concurrency.
>Another issue is just that the conceptual mental model for React is just functions calling other functions recursively. There is a lot of value to express it in those terms to help build the correct mental model.
For a concrete example of where classes as a model fails us, consider useTransition (https://react.dev/reference/react/useTransition). It lets you start rendering "in background" with a different state value. But if you get interrupted, the renders have the current value. This highlights that in React, the same piece of state can conceptually be thought of having more than a single value (kind of like being in parallel worlds). Classes don't model that well.
The operative word there is 'exploring'. In practice, hooks are still basically OOP, albeit masquerading as FP. You just shuffled the state into a shadowy realm adjacent to the component where it's harder for developers to see. I know a lot of JS developers are allergic to writing the 'class' keyword (even though JS doesn't really have classes in the first place), and I guess hooks help them sleep easier at night by allowing them to believe that they're writing their code in a functional way. But they usually aren't. And I think that misdirection is the source of a lot of the confusion out there regarding hooks.
"The greatest trick the OOP devil ever pulled was convincing the world that state didn't exist"
I really don't think React is OOP. There is no misdirection here — it really is a different conceptual model.
Even if we set aside implementation inheritance, class hierarchies, and all that jazz that got associated with modern OOP, fundamentally classical OOP is message passing. React components don't pass messages to each other in that sense. The data flows strictly down. Re-rendering is not message passing but conceptually reevaluating a part of a lazy continuously reactive function call tree.
It's not about "sleeping easier at night" etc, it's just a different model. If you're curious to entertain this idea for a bit, I have an article you might enjoy reading: https://overreacted.io/react-as-a-ui-runtime/
I did enjoy that article; I think it's a great overview of React's essential architecture. But I don't think it obviates the point I'm trying to make.
> fundamentally classical OOP is message passing
OOP in its essence is about using conceptual data models as state containers. Cats, trees, input fields. Message passing is just the means by which these containers interoperate. The persistent state is the defining element.
Judged in this light, I'm arguing that the way most people write React components is more OOP than functional. The components maintain some state inherent to their nature. And I would argue that these components are still passed messages, or at least one: "render". When a component is passed the "render" message, it uses its internal state to output the appropriate DOM structure. A news widget maintains the list of headlines to display. An expanding menu holds the state for which menu items have been expanded. A form holds the errors that have been triggered from input field validation. And so on.
Thus, each component is still essentially a polymorphic implementation of an interface with a "render" method: an "instance" of a "class" that encapsulates state, even if the actual language-level class doesn't exist anymore. The mechanics are essentially the same. Since "render" was the only method being invoked on the component class, you took that lone method and promoted it to a standalone function that is invoked directly. And then you took the internal state of that class, and moved it behind the hooks API. That way, the "render" method can still access the class's internal state despite the absence of the "this" keyword. That's what I meant by misdirection. The class has just become virtualized.
Obviously it's possible for people to write React code in a way that avoids local component state. Naively we could just do prop drilling, in which case your argument that "the data flows strictly down" would actually be true (as opposed to being commingled with the data that is being stored in local component state at various levels). Or we could use Redux components, which, when written "correctly", are essentially pure functions that accept the entire application state as an argument. Et cetera.
Maybe that's actually your philosophical vision for how people should be writing React code, in a frictionless-plane-of-Platonic-ideals sort of way. Personally, mine is pretty close to the Redux model: components are just a Pachinko machine of nested functions that emit UI, and my application state lives outside of that machine, and is fed to it as an input.
But that's not how most people actually structure their React code. Instead, they continue to model their application as an aggregation of conceptual objects a la OOP. Forms. Widgets. Color pickers and the like. Each with its own persisted state. The fact that you've architected React to avoid semantic classes is orthogonal to the fact that people are still using it to implement conceptual classes.
> It really has a bunch of powerful properties one might not expect.
Can you elaborate on this?
> a class is a function, but that function is invoked once. Its methods can be called many times, but having a pure render() method (like in class-based React) is really a class cosplaying as a function. Functions are more honest to what React is trying to be
useState() is a function cosplaying as a class ;) and that's really my biggest hangup. It seems like react components are not quite functions nor are they classes but rather somewhere in between... And because of JS quirks and perhaps the internal architecture/mental model, functions end up being a better choice.
> in React, the same piece of state can conceptually be thought of having more than a single value (kind of like being in parallel worlds). Classes don't model that well
Maybe I'm missing something but this seems like a false dichotomy because the equivalent in the class-based model would be the render function, not the class itself. And in that sense, your set of render functions can also have multiple values given a state. Though just writing this out does make functions feel a bit simpler.
Admittedly, you've mostly convinced me! And after reading through the new documentation, going function-only feels a lot cleaner than it did when I learned react using the old docs.
In other paradigms, it is! Our paradigm is exploring the functional take. I agree it's a bit unorthodox but we are very intentional about modeling it that way. It really has a bunch of powerful properties one might not expect.
>And aren't classes just really functions under the hood anyways?
The key difference is that in React, UI is a pure projection of current data (props/state). You're always supposed to "return" the UI. Sure a class is a function, but that function is invoked once. Its methods can be called many times, but having a pure render() method (like in class-based React) is really a class cosplaying as a function. Functions are more honest to what React is trying to be.
Relevant part from Seb's Hooks RFC comment (https://github.com/reactjs/rfcs/pull/68#issuecomment-4393148...):
> Classes may seem like the ideal thing to hold state since that's what they're designed for. However, React is more written like a declarative function that keeps getting executed over and over to simulate it being reactive. Those two things have an impedence mismatch and that keeps leaking when we think of these as classes.
>Another issue is that classes in JS merge both methods and values on the same namespace. This makes it very hard to make optimizations because sometimes methods behave like static methods and sometimes behave like values that contain functions. The Hooks pattern encourages the use of more statically resolvable calls for helper functions.
>In classes, each method has its own scope. It causes issues like us having to reinvent default props so that we can create a single shared resolved object across those. You also encourage sharing data between those methods using mutable fields on the class since the only shared thing is this. This is also problematic for concurrency.
>Another issue is just that the conceptual mental model for React is just functions calling other functions recursively. There is a lot of value to express it in those terms to help build the correct mental model.
For a concrete example of where classes as a model fails us, consider useTransition (https://react.dev/reference/react/useTransition). It lets you start rendering "in background" with a different state value. But if you get interrupted, the renders have the current value. This highlights that in React, the same piece of state can conceptually be thought of having more than a single value (kind of like being in parallel worlds). Classes don't model that well.