Developers have never been shy about disliking certain React APIs. They feel awkward, restrictive, or just plain counterintuitive. But the reality ...
For further actions, you may consider blocking this person and/or reporting abuse
Honestly, the dependency array really annoys me. I always miss something, and then the whole site goes berserk.
I think one difference with Signals is it won't update if you miss. And you shouldn't be reading them in the effect itself so it will basically tell you if you miss something.
There are React recommend eslint rules to prevent that from happening
honestly the hooks dependency array is the one I've just made my peace with. it felt wrong at first - like you're describing side effects to the compiler instead of just writing them. but once I stopped fighting it and started treating it as documentation of what the function actually depends on, things clicked. still think the mental model is genuinely awkward to explain to people new to React though.
The uncomfortable truth you're naming: signals didn't fix the async problem, they just delayed it. React's awkwardness wasn't a mistake it was the shape of the constraint becoming visible. The frameworks that pretend async is a special case are the ones that will break first when the graph gets complex enough.
Thankfully my work on Solid 2.0 has shown me that these don't need to be mutually exclusive things. We can consistently address Async and still keep all the advantages of Signals. We just have to be open to it.
The difference is you're building the constraint into the design instead of pretending it doesn't exist. That's the line between a framework that teaches you something and one that just gets out of the way until it doesn't. Looking forward to seeing where Solid 2.0 lands.
There’s something funny about realize why something that didn’t make sense at the time makes perfect sense after facing the problem it already solved. Really humbles once self. Thanks for sharing your experience. Thanks for reminding me to never stop learning.
Agreed
This is one of the most intellectually honest pieces I've read about framework design. The way you distinguish between React-isms and actual invariants that any async UI system must confront is refreshing. Most people stop at 'React bad, Signals good you've actually done the hard work of asking why React made those choices and whether they point to deeper truths. Respect.
Interesting read, thanks Ryan!
I guess that's why I always disliked the "component as function" thinking in React. I actually like the primitives (HTML, CSS, and JavaScript) and understanding it as a render loop, that simply displays the state as it is, in the moment of the tick, is actually sufficient. You just fill in the "gaps" (async resolves) when they are available. So Svelte5 way is much more natural to this and "more correct" for my understanding.
I mean Svelte 5 is basically Solid 1 mechanically (until they added the new async stuff). I don't think the container being a function or not matters. React's rerenders do versus Svelte/Solid's surgical reactive updates. But that doesn't completely tell the story though because the changes themselves are still subject to the same constraints as React even if more granular. Like the physics is undeniable.
Svelte 5's async solution is aware of these constraints. They discover deps early through compiler extraction of the await keyword to block at that scope. So to be fair they can solve the split effects issue without the split. But it comes at the cost of potentially higher blocking and graph coloration. But the fundamental truth is the same.
Svelte 5 doesn't defer flush but it also doesn't carry the same consistency guarentees. It is subject to the breaking I was talking about in the article but honestly it probably doesn't happen too often. People really shouldn't do much read after writes. So while React's approach is undeniably more correct there it probably won't bite you.
OK. Thank your for answering, I'm not very familiar with the underlying implementation details of Svelte5 and/or Solid and come from a framework user perspective. So my point was basically, that Reacts "functions approach" felt misplaced, because I don't want to think about the problem (displaying HTML based on state) in a functional way (idempotent - the input defines the output).
I guess I can see/understand your point. But I also don't see why it has to be so restrictive. So because of the async nature of the web, I don't expect the view to be in complete sync, like a database transaction, all or nothing. Why should I expect "$derived" to be in-sync/simultaneously change with it's "$state". And I also can see how you can code your self into weird situations. But that's an inherent property of async stuff. Not so much a problem a framework can solve, without making it sluggish (after 15 levels of nested $derived with async calls mixed in).
You can not / or should not abstract async away from the user, but maybe that's your whole point.
Yeah as a framework author those consistency goals are very high priority as to have users not worry about it. Runes(Signals) are exactly that for synchronous updates, and as you explore new async features in Svelte you will see similar tie in in the UI, you won't see state update until async derived on it do. The part I don't cover in this article is the other side of the representation. In Svelte they have eager, and in Solid we've been building a number of helpers to basically allow show state both in current and speculative states. This is all very fresh so I expect shifts over the next couple years as it all unrolls.
So the idea isn't to hide async but better adapt it to our declarative representation of state.
When I'm interpreting your right, you propose to wait for all dependent $derived (which may include async, e.g. fetch calls) to be resolved before actually updating the $state.
If so, I have the following problem with that.
The original $state is bound to all the dependents, which the component/state class doesn't know. I see you could use "eager" to actually present the (maybe) coming change, but that's extra work, and introduces more problems, like the user interacting with the state again, before the first roundtrip finished.
Since the state change, is usually coming from UI interaction, this will make things unpredictable and/or sluggish (if you wait for dependents to be resolved).
My point might be actually "why bother". Can't you just treat the $derived as generally async. Update $state immediately, call and forget all $derived which call their $derived ... and so on. When a $derived is called before the previous call has finished, just throw the previous execution away and call it again.
You then have the problems with side-effects inside derived, bluntly speaking, don't do it.
Sorry for the Svelte wording, that's what my brain is currently wired to. And sorry again if that's naive.
Maybe you could even make the $derived communicate that they are "pending", so that you don't have the mismatch (your 2 * 2 = 2 example, would become 2 * 2 = [calculating]). This information could be synchronously communicated to all dependets (in a compiled world). Actually when I think about it, it's probably a similar pattern then the SvelteKit RemoteFunctions (where everybody acknowledges their async'ness).
Isn't an answer to the asynchronicity problem in types? It seems complicated because standard
Promiseimplementation has no state and can not observe resolved/unresolved status. But this can be fixed with a simple subclass: github.com/canonic-epicure/siesta/...After that, one can simple wrap the
PromiseSyncvalue into a signal and then, on the consuming side, differentiate the resolved/unresolved and render different content based on that.With this approach there's no need for asynchronicity in the reactive layer. Of course that requires a dependency on the internal "resolved" field, so need a special kind of signal.
It helps if sync and async resolution could be handled more smoothly. But the coloration of async does sort of contaminate everything. I also don't think it removes the need. Like sure it works, but composition/derivation still needs to flow and ultimately the hardest choice for a lot of these is setting async boundaries. This impacts things like streaming but also gives us clear loading boundaries that can discover their async via inversion of control. Like you don't know all th async that will be below you, but you'd like to capture it. For those mechanisms to work you need to stamp somewhere in the UI where we care.
And for Solid I think it makes sense to make that in the front half of rendering effects because that is the most leaf you can be. Whereas other systems end up using compiled
awaitsor like auseto do that a more component level. Seems very restrictive in a similar way that I find React's state to component coupling restrictive.I agree that setting proper boundaries to avoid excessive coloration / changes contamination is important. I believe this is a stylistic change on the consuming side:
This requires users to be educated of the "reactive boundary" for the values they use in the templates. Probably a matter of good docs.
Same story with dependency arrays. Teams hate writing them, but large React codebases quickly become unpredictable without them. In one of our projects we tried hiding dependencies behind custom hooks. Six months later debugging effects became painful because nobody could tell what actually triggered them. Explicit dependencies looked ugly, but they made the update graph visible.
I think side effects is where this most often manifests. Pure computations usually require all their deps and it is pretty clear the path. Effects tend to close over a different world so to speak so it's why the separation clarity is more important.
A few ways of looking at things 🤝
The flaw in React is the rerender of a publish/subscribe pattern. The publish subcribe pattern should be outside of its own dependencies.
UseEffect is essentially a subscribe pattern
UseState or any other data input is the publish pattern.
A few useEffects is fine but realistically any decent sized monirepo will exceed this. So to overcome this perpetual problem we use useRefs and useCallbacks to detach the publisher from the rerender. It looks ugly but we accept this floor because React encapsulation of components has become the easiest front end library.
What you are describing is neither easy or simple. At this point component encapsulation is everywhere. At the point you need to break the update flow the way you are describing you'd be better reversing the model. Ie.. components don't rerun. People need to look outside of React. I think it would help even if continuing to use it to find better patterns.
It seems like the core problem is the mixing of db state with local state. Hypermedia driven applications solve this issue as you would update the entire thing at the same time or do a diff on the DOM state itself.
Maybe I'm thinking too simplistically, but thinking about the issue you have addressed that is how I would address it when I need to communicate with a database on the back end. Yes, there are other reasons you might need to contact some other service, but even then, I would do that on the back end and get the info first from the external service and do the update all at once - similar to interacting with the DB.
This has been one of those hard but good paradigm shifts for me to make. Honestly, the first read of this made me want to go back to synchronous signals. Admitting that I didn’t fully understand and coming back to this article with more curiosity is what brought me around. Understanding why these things are necessary requires both understanding of reactive graphs and good intuitions about problems that developers don’t run into at first but inevitably run into further down the road. I’m grateful that we have you to guide us through this!
OK actually I do still have one nitpick. I don’t think returning null, undefined, or { loading: true } breaks determinism. It still represents a moment in time, namely a moment when something is loading. It does cause branching, and I agree it’s better to not need to branch everywhere.
Interesting how Vue does not inherit any of these issues with composition api.
Vue’s reactivity and weak map dependency tracking system nicely abstracts the problem of async vs sync by simply decoupling the template from the computed values.
In other words the pain point/problem here is with how in React a function/component is both the state and the output/render making them tightly coupled as susceptible to these side effects.
In Vue if you have full control if you want to display 2 * 1 = 2, 2 * 2 = 2, 2 * 2 = loading, 2 * 2 = error, until the async is resolved and ready to update the UI. And the beauty of it is that the 2 references for the multiplier and the result don’t have to be directly related or coupled with the template where they are displayed. You can put bind them to 10 places in the UI without no care in the world about tracking dependencies, thinking about render cycles, considering one or many async chained dependencies… it just works.
And that’s not the most amazing part. That’s just what you get out of the box. What makes Vue unique is that it provides all the control for cases where you might want to manage state based on component lifecycles via onMounted, onUnmounted, onUpdate, onScopeDispose and the various router states and guards.
In my job we use react and we literally have daily open hour meetings where we have to explain and actively guard our codebase against react idiosyncrasies cause of the core problems you are describing. It’s unfathomble how much resources, time and compute is lost simply due to this fundamental react design flaw.
Unfortunately it isn't quite that simple. Most frameworks have a way of mimicking all the different scheduling behaviors via other primitives. That being said Vue is actually susceptible to the problem I talk about in this article. All signals based frameworks are. As I said in the article we sort of push back the problem but it is still there. Everything I said is relevant to Vue.
I say this as a long time enthusiast of this sort of Reactivity. React might hold a fairly limited model, but it is right about these 2 things. In an async first world if you don't defer commit the model falls apart. It isn't about the UI, that can be modeled a number of ways with signals, but the consistency of state graph.
Even with the React Compiler the dependency array for
useEffectis still required to avoid running the effect after every render.Currently but my understanding is they had a solution for that: react.dev/blog/2025/04/23/react-la... . That being said Svelte 3 basically did it and it isn't too hard to see how React would.
"We aren’t adopting these constraints because we’re boxed in — we’re adopting them because they are true. Async forces commit isolation. Async forces effect splitting. Async forces a consistent snapshot. These aren’t React‑isms; they’re the physics of UI.
Embracing this isn’t mimicry. It’s maturity. It’s choosing the inevitable path with eyes open, and building a system that treats async not as an edge case but as a first‑class part of the architecture. It’s the next step in making Solid not just fast, but fundamentally right.
Clarity doesn’t simplify the world, but it does make the direction unmistakable."
I usually enjoy your writing but most of this article is exhausting to read, and I'm not against generating text. You can use a writing skill to avoid these LLMisms, or add another pass by an editor-agent to remove them.
Forgot the dependency array in a useEffect once. Ended up spending two days debugging it.