My sense is that in React, the complexity comes from the management of minimizing the "blast radius" of state changes to prevent over-renders. So there are a lot of different approaches and ways that folks have cleverly engineered to solve this problem, but none of them feel as simple as say Pinia on Vue, for example.
You have your app state in an object/class, and components automatically rerender when the part of the store changes that they access during render.
class Store {
counter = 0
constructor() { makeAutoObservable(this) }
increment() { this.counter++ }
}
const Component = observer(() {
const store = useStore()
const click = () => store.increment())
return <button onClick={click}>{store.counter}</button>
})
It has a very light set of idiosyncrasies for what it gives you unlike, say, redux.https://github.com/Facepunch/react-class-model (note: context is not required)
It has a lot of expressivity but is incredibly brittle and dangerous.
The cost of a round trip for every UI interaction might seem high, but I've never seen a distributed client/server state machine model that could compensate for any of these alleged UX harms without simultaneously bringing in more complexity than anyone was prepared to deal with.
I also love the simplicity of SSR/vanilla web for some things. But I say keep offline-first SPA/PWAs alive. Cross-plaftorm. Freedom from the app stores. Freedom from needing to be tied online all the time.
But there are good and powerful use cases for client side rendering.
I have no problem with the properly offline-capable apps using standards compliant web technology. I've been championing the use of PWAs to circumvent the iOS App Store for years.
To be very specific, the problematic solutions in my view tend to be those right in the middle of pure SSR and pure client. They aren't sure if they are always online or offline by default and the actual customer use cases don't seem to have been fully explored.
Did you ask the customer if a ~150ms form post to us-east-1 from their main office would be something they'd even care about? Why not? You could save an unbelievable amount of frustration if they don't care. It would take 10 minutes to set up a demo they could try if you think it's going to be a deal breaker at delivery time.
I've not once worked with a B2B customer in the banking, insurance, finance, manufacturing or retail domains who raised a concern about any of this. Only nerds on the internet seem to fantasize about the customer getting outraged at a browser navigation event or a synthetic browser benchmark score.
Nor would I call it simple that the server has to be able to send the whole UI over the wire instead of just data. And it ties your server to the web browser instead of providing a simple data API that can be used by anything that can make an http request.
There are always trade-offs.
The number of broken stateful server-side page that had some weird server state and couldn't handle URL navigation?
You can f up either way, but I've seen as many crimes with server apps as with client ones.
You know, about 7 years ago I would have heartily agreed with you. KISS, right?
The thing is, it just doesn't make financial, UX, or security sense to do that. The cost of storing every jot and tittle on the backend is huge. The collateral of anything happening to the backend becomes larger. Enjoy benign things like preferences/app settings, unsent comments not having to be rewritten because your session expired, etc? If you're not storing them via local storage, you can KISS that goodbye.
If you write React code that strays from that model, you better know what you're doing. When I have to reach for `useRef`, I know that I'm in dangerous water.
Of course many actions logically cannot be made idempotent, but for the subset that can, do it as much as possible.
- is the default way that most humans interact with computers
- is an extraordinarily broad problem space and solution space
- is something that basically every modern software company needs to do in some capacity, and with even a few developers, abstractions become desirable
I'm not saying there wasn't a better way for React to adopt FP principles. I certainly have my own gripes.
But to start a conversation with "why does the most widely adopted framework for the most broadly-used software interface in history have some rough edges?" seems, to me, to be sort of begging the question.
The one project I've used that had redux was also a complete nightmare to work with, and hooks are the blessed way now apparently.
The amount of times I've heard that classes = OOP and functions = functional is ridiculous
Not really, though. While you can model any program as a state machine, doing so typically adds nothing to your understanding of the program.
State-machine diagrams are especially useless. A diagram is only useful if it has about a dozen things in it, maybe two dozen, maximum. But that doesn't mean you can model a dozen states in a state-machine diagram; in these diagrams, the transitions, the arrows between the states, are at least as important as the states themselves. So the diagrams are useless when the number of states + the number of transitions between them are greater than a few dozen. Typically that means state diagrams are only with a handful of states with a few transitions hanging off of each.
But, if your problem is simple enough that you can represent it with a handful of states with two or three transitions each, then you have a very trivial problem, so trivial that the state-machine diagram likely added nothing at all to your understanding of it.
This is why no popular UI frameworks actually do model UI as a set of states with transitions. It's easier to model the state diagram as a function and then just forget about the state diagram.
That's what React is all about! A pure function, UI = f(state). It's better than a state diagram.
This article is saying: "Hey, you could think of React, something conceptually simple, as something unnecessarily complicated, instead!" Gee, thanks?
The observation I'm trying to capture in this post is that even though we don't define a formal transition table, we actually _do_ implicitly define the set of valid (user) transitions for each state via the event handlers we bind into the DOM when our React component tree renders that state.
And yet, it's extremely common to see apps with clearly broken states and state transitions for what should be relatively "trivial" state machines. Think play/pause buttons, buttons with loading states, form fields with error states, etc.
For example some people reach for it for fetching data in routes. It wasn't until recently that routers started to come with built in patterns to do this without effects - example being react routers (it's been a while since I used but remix had this) loaders and tanstack routers beforeload and loaders.
People fetched in an effect because at the time this was the most simple and obvious place to do it unless you were very aware of the other ways to do it, and we didn't have primitives like react query either.
Another example of a non obvious things logging an analytics event when a specific component renders. You could attempt to tie this to the specific update that caused a component to show but that update may be very complex or hidden since your component could show based on a variety of states and require so much more work vs just logging in an effect.
I guess one could argue both of these themselves are syncing operations, syncing your network state and server state for API requests in routes and analytics. But at the same time reacts team told everyone if you're fetching in effects you're doing it wrong without providing the answers.
That to say yes effects are not a very good pattern for many things we use it for and should be avoided, but react as a framework (yes it's basically framework in my opinion, we're well past just. library point) itself does not educate well or have easy to understand built in ways to do these things that are VERY common use cases
And this as someone who writes mostly react.
Unfortunately, many people use inappropriate levels of abstraction (useEffect and/or redux) and it becomes an architectural problem.
But a few instances were to conditionally set one piece of state whenever another piece of state was changed, arguably an abuse of the mechanism. I suppose the proper way would be to wrap the setter function into one that changes both, but it takes a fair bit of discipline to avoid useEffect in the heat of the moment.
I really like React, but given the way developers seem to struggle to use it “correctly” (despite all the lint hooks and weird diagnostics like double rendering to help) it’s hard not to feel like there’s something wrong with it.
That use case is explicitly called out on the "You Might Not Need An Effect" article in the docs (which everyone writing React should read, and arguably was published years too late): https://react.dev/learn/you-might-not-need-an-effect
TLDR:
When updating a useState based on another useState, don't use the first useState at all, just compute it inline. If it's expensive, wrap it in a useMemo.
When updating a useState based on props, call the setter directly from the component function, and React will immediately re-render the component (instead of rendering it, running the effect, and then rendering it again).
> When updating a useState based on another useState, don't use the first useState at all, just compute it inline. If it's expensive, wrap it in a useMemo.
Well, the problem in this case was that the affected useState was not just a pure function of the useState that caused it to be modified: other actions could modify it as well. (E.g., you have some form state that should get updated whenever a state value somewhere else is changed.)
I believe useReducer is a bit closer to the use case I'm thinking of, but the dispatch functions for that are verbose and unpleasant to write. Presumably ad-hoc closures wrapping the setter functions would be somewhat more lightweight.
Both paradigms have foot guns but having used both I much prefer the hook version.