DEV Community


Posted on


On state mutation on changes of state

In this post we will investigate a interesting but rarely thought "problem" of reactivity: state mutation inside effect caused by a previous state mutation.

In this post, the usage of word 'state' is in the context of reactivity - capable to raise (side) effects.
e.g. createSignal - createEffect in solidjs, ref - watch in vue, even React Hooks can fall into this category.

How is that a "problem"? Well obviously there is a recursion-like structure or circular definition in that sentence - like state mutation inside effect caused by a previous state mutation inside effect cased by a previous state mutation...... maybe it will eventually stop at some point or never stop. I personally didn't dislike recursions, I want my code to be predictable though. So if I really need a recursion, I will try to make it as local as possible (or convert to an iteration). However, reactivity inevitably inverts your flow of control. You are asked to set the (source) state and never care what's going to happen (the merit from Separation of Concern), so you are not sure about whether the current mutation will cause another state be mutated, unless - state mutation inside effect is completely forbidden.

How is that possible? I rephrase the question - Is there any functionality that is only achievable by state mutation inside effect?

My answer is no, if you don't mean to construct a recursion. The secret is the implicit dependency and circular reference possibly caused by that implicit dependency. The mutated state in effect is implicitly depending on the state(s) raising that effect, that's intuitive. And if the latter is already explicitly depending on the former, there is a circular dependency constructed. If the recursion never converges , sometimes the reactivity implementation will be able to identify this, but that's just better than getting stackoverflow or infinite loop if the implementation "smartly" utilises the eventloop. Actually some recursion can be proved to converge but most of time you get a convergent recursion accidentally to work, and it is like a landmine that is ready to f-word up everything when you start refactoring (or you cordon off code "LEGACY CODE, IT'S WORKING, DO NOT TOUCH"). Anyway you are not advised to implement recursion based on reactivity (hard to maintain because the control is inverted), and every-time you construct a recursion is by accident. So state mutation could be simply banned for recursion construction.

But what about those non-recursion cases? Since not all mutations will form a circular dependency. Well most of reactivity implementations provide a functionality to declare a state derivation (like createMemo in solidjs), that's an obvious solution if a state unconditionally depends on another one. As for conditional derivation, just one more thing, the previous value (which might be provided already in the API, or store the latest state into somewhere non-reactive (e.g. useRef of react) when effect triggered), so if the condition is false, just return the previous value. In conclusion we are converting implicit dependency into explicit one.

I'm just about to say you don't need state mutation inside effect.

But I soon come up with a made-up use case: there are state A,B,C and two effects mutating C triggered by corresponding changes of A,B. It's neither a case of circular dependency nor solvable by methods mentioned above. So far I think this is an anti-pattern, as the concern of control flow is leaked into reactivity - what if A, B changed at the same time, the execution order of effects does matter, while a reactivity implementation does not necessarily offer guarantees.

What's your opinion? Feel free to talk about in comments.

Top comments (0)

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓