Could you say more about this? I'm not a React user, but to me one of the OO fundamentals is "object = behavior + state". What you're saying sounds so obviously correct to me that I guess there's something pretty weird going on in React-land?
Yes, there is something very weird indeed. Functional components are called every time they're rendered (that's not weird). But they have to maintain state between calls; they can't start over again fresh for each call/render (still not weird). So how do they do this? `const [state, setState] = useState(initialValue)`. You might look at that and think, I see useState being called, so it must be called on each render, so state is still not being preserved between calls.
But here's the weirdness. useState knows whether it's already been called for a given component; this is how it tracks state. The first call returns the state (the initial value) and a setter. Subsequent calls -- occurring after the first render -- do not return these objects anew, but instead reach into a per-functional-component store and return the values that already exist there. Calling the setter doesn't set the state in the body; rather, it updates the value in the store, then triggers a re-render.
If you're wondering just how react knows how to match up calls to useState between renders, it doesn't really. It simply matches them up based on the order they're called in. For that reason, you have to always call exactly the same useState calls on each render -- no putting some in an if statement or a variable length for loop, otherwise they'll be out of sync.
There are a bunch of other "hooks" for use in functional components, but they all work basically the same way: place some data in the functional component's store, then set up re-render (which, again, just means calling the component's function again) to occur when certain data changes.
It is bonkers. My number of "accidental" re renders on react are greater than before hooks.
Before at least I knew what was called on boostrap moments. Now everything is run as a side effect when any dependency change. My needs on checking if a prop or a dependency has changed hasn't evolved at all. I just have the core of my side effects on useEffect.
It sounds bonkers, but one of the reasons I pushed for moving my company’s app to React was because the useState hook reminded me of Reagent’s ratoms. I believe that was at least part of their intent. The catch is that you can’t really enforce immutability or atomic updates in plain JS, so they built hooks into the library as a workaround.
There’s not much of a cognitive difference between “defining a reagent component which uses ratoms and calls a pure function that derefs, updates or swaps them”, and “defining a react functional component that uses hooks”. It looks weird to have the “atoms” inside the render function instead of outside, and there are the aforementioned limitations (“the rules of hooks”), but it’s a compromise to get the feature into JS React in a consistent, performant manner.
I’d love to use CLJS/Reagent or even go full re-frame on our front-end codebase, but that’s a hard sell at my company since I’m the only dev who’s ever played with Clojure.
It's sort of what happens when OOP is demonized for years and FP is lionized. They've created what are effectively classes but with implicitly auto-generated private property names and a lot of weird edge cases where things can go pear shaped. But hey, it's functional and 'new' so it's good, right?
In fairness, the JS world has never had well engineered OOP GUI toolkits like you find in the desktop space. If you've never used JavaFX then ReactJS probably seems pretty magical. If you have, then, well ...
I know, but you didn't actually say you think React is awesome when not using JS. Given how JS specific React is, that's not a very intuitive outcome.
The comment was in response to "that's bonkers". And that has been my reaction on learning stuff like React and derived frameworks. A lot of it looks like functions for the sake of it, when objects already solve those problems but became unfashionable.
React added "hooks" which are basically methods and properties implemented as FIFO queues instead of lookup tables, with terrible syntax that requires you to declare them inside their constructor (the "functional" component's... well, function).
No, I'm not kidding.
Unless something's fundamentally changed about the code since release, they even end up attached to an object representing the UI component, by the time the heart of React's runtime code considers them. It's some real Rube-Goldberg shit. I read the code because I read the announcement docs and was like "wait, it looks like they... but no, surely they didn't" but yeah, turns out, they did.
What's worse, they have interesting "rules" that one really needs to use a linter so their IDE/Text editor gives them friendly reminders. One cannot conditionally call useEffect. One needs to add all dependencies to useEffect's dependency array - BUT that has potential to cause infinite re-renders (especially when using a getter/setter pattern with useState). They encourage DEFINING functions inside of other functions. Years of CS education and practice go right out the window because some popular JS person on Twitter says, "it's fine".
When pressed about it, be ready to be hit with, "You don't understand hooks".
I agree with the others. I've started using hooks since they were introduced and I've only hit the re-render issue only a few times, which I promptly fixed as they were caused by carelessness. My strategy is to not use a single useEffect, but multiple ones for each (which reduce greatly the dependencies). Also, avoiding dependency circles between useState and useEffect.
I've used class components and I think hooks are much better than the monstrosity that the lifecycle methods would usually become.
Also defining functions inside other functions is very much the staple of functional programming.
In React terms, "functional" almost always implies the opposite of purely functional. It just means that the component is declared by a function instead of a class. First time you call it, the function can assign state that it may reference in future calls, not unlike the methods of a class. How they actually go about this is however almost entirely weird.
Classes in JS are just functions anyway, and functions can have state. Saving the state of a function is a different thing, but of course JS allows that, too. Why is it weird?
Class instances are objects and it's the objects that have state. Functions can only abuse themselves as objects to store state.
function johnson() { johnson.state = {}; }
This is however shared state and the state will be reset whenever the function is called a second time. So what you normally do with a function is to pass the state via arguments and now the same function can work with different states. React could totally do this via props or as a second argument.
function Johnson(props, state) {}
Instead they changed the laws of functions so that a function can act different the first time it is called and also be called the first time multiple times if and when the function calls a functions that looks like JavaScript but follow different rules [1].
Functional components are great in that they are more terse, less boilerplate.
Class components just have more lines, more boilerplate, so they have a higher cognitive load. But when you have state, you have more complexity, you can't just wave your hand wave and make it go away. You need to take the complexity into account.
Hooks are trying to do hand waving. The thing is, once you get to non-trivial use cases, the handwaving stops working. You're better off thinking a lot about where you state lives in your component hierarchy, and then limit your state to where you really need it. Once you do that the overhead of class components doesn't really make that big a deal.
The same principle applies to almost everything in programming, the more thought you put into structure, the simpler you can make everything.