DEV Community

Cover image for Two React Design Choices Developers Don’t Like—But Can’t Avoid

Two React Design Choices Developers Don’t Like—But Can’t Avoid

Ryan Carniato on March 13, 2026

Developers have never been shy about disliking certain React APIs. They feel awkward, restrictive, or just plain counterintuitive. But the reality ...
Collapse
 
ujja profile image
ujja

Honestly, the dependency array really annoys me. I always miss something, and then the whole site goes berserk.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
brense profile image
Rense Bakker

There are React recommend eslint rules to prevent that from happening

Collapse
 
itskondrat profile image
Mykola Kondratiuk

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.

Collapse
 
eaglelucid profile image
Victor Okefie

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
eaglelucid profile image
Victor Okefie

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.

Collapse
 
rafde profile image
Rafael De Leon

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.

Collapse
 
heckno profile image
heckno

Agreed

Collapse
 
harsh2644 profile image
Harsh

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.

Collapse
 
pjs_217e4875ea profile image
Paul Spaulding

Interesting read, thanks Ryan!

Collapse
 
crenshinibon profile image
Dirk Porsche • Edited

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
crenshinibon profile image
Dirk Porsche

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.

Thread Thread
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Thread Thread
 
crenshinibon profile image
Dirk Porsche • Edited

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.

Thread Thread
 
crenshinibon profile image
Dirk Porsche

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...

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).

Collapse
 
chronograph profile image
Nickolay Platonov

Isn't an answer to the asynchronicity problem in types? It seems complicated because standard Promise implementation 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 PromiseSync value into a signal and then, on the consuming side, differentiate the resolved/unresolved and render different content based on that.

const async_value = SIGNAL(new PromiseSync((resolve, reject) => {
   resolve(FINAL_VALUE)
}))

const component = <div>
    { 
        async_value.value.isResolved() 
            ? RENDER(async_value.resolved) 
            : RENDER("async in progress")
    }
</div>
Enter fullscreen mode Exit fullscreen mode

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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 awaits or like a use to do that a more component level. Seems very restrictive in a similar way that I find React's state to component coupling restrictive.

Collapse
 
chronograph profile image
Nickolay Platonov

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:

const component = <div>
    <div id="div1">
        { //usage of async1.value - does not affect div2 }
    </div>
    <div id="div2">
        { //usage of async2.value - does not affect div1 }        
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

This requires users to be educated of the "reactive boundary" for the values they use in the templates. Probably a matter of good docs.

Collapse
 
playserv profile image
Alan Voren (PlayServ)

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
playserv profile image
Alan Voren (PlayServ)

A few ways of looking at things 🤝

Collapse
 
steve_tomlin_ced0d3c7183c profile image
Steve Tomlin • Edited

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
jon49 profile image
Jon Nyman

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.

Collapse
 
jrood profile image
John Rood

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!

Collapse
 
jrood profile image
John Rood

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.

Collapse
 
cmacu profile image
Stasi Vladimirov

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.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
acutmore profile image
Ashley Claymore

"Could a compiler extract dependencies from a single effect function? In a shallow sense, yes — React’s compiler does exactly that"

Even with the React Compiler the dependency array for useEffect is still required to avoid running the effect after every render.

Collapse
 
ryansolid profile image
Ryan Carniato Playful Programming

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.

Collapse
 
columk1 profile image
Colum Kelly

"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.

Collapse
 
jit_chakraborty_4222410eb profile image
Jit Chakraborty

Forgot the dependency array in a useEffect once. Ended up spending two days debugging it.