[Concurrent] Safely disposing uncommitted objects
See original GitHub issueHow to safely keep a reference to uncommitted objects and dispose of them on unmount?
For a MobX world, we are trying to prepare for the Concurrent mode. In short, there is a Reaction object being created to track for observables and it is stored within useRef
.
The major problem is, that we can’t just useEffect
to create it in a safe way later. We need it to start tracking the observables on a first render otherwise we might miss some updates and cause inconsistent behavior.
We do have a semi-working solution, basically, a custom made garbage collector based on setTimeout
. However, it’s unreliable as it can accidentally dispose of Reactions that are actually being used but weren’t committed yet.
Would love to hear we are overlooking some obvious solution there.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:17
- Comments:77 (22 by maintainers)
Top Results From Across the Web
use-dispose-uncommitted
useDisposeUncommitted is a tiny React hook to help us clean side effects of uncommitted components. Based on similar implementation of MobX's internal hook....
Read more >Mike | grabbou.eth 🚀 в Twitter
How to safely keep a reference to uncommitted objects and dispose of them on unmount? For a MobX world, we are trying to...
Read more >React hook for disposing uncommitted logic : r/reactjs
use-dispose comes into play by guaranteeing that the created, uncommitted object can be disposed during the render phases.
Read more >use-dispose-uncommitted - npm package
Learn more about use-dispose-uncommitted: package health score, ... the React repo on this topic: #15317 [Concurrent] Safely disposing uncommitted objects.
Read more >Finalization of ConcurrentBag containing unmanaged objects
When code is executing from the finalizer ( disposing is false ) the only things you are allowed to do is use static...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
We’re going in circles but let me reply to this. People can write any kinds of libraries — we can’t stop them. But the React component contract has always emphasized that the render method is meant to be a pure function of props and state. This makes it throwaway-friendly. It is not intended as a constructor, in fact it’s the other way around — it’s React classes that act as poor approximations of functions. Which is why React is converging on a stateful function API now.
If a library chooses to ignore the rules and mutate a list of subscriptions during render, it is its business. But it shouldn’t be surprising that when we add new higher-level features to React that rely on the component contract having a pure render method, those features may not work. I think it’s fair to say that render being pure is well-documented and one of the very first things people learn about React.
@gaearon @urugator’s description is pretty accurate. Whether it should be fixed through a React life-cycleish thing or by doing some additional administration on MobX side (timer + custom GC) have both it’s merits. So let’s leave that aside for now, and just focus on the use case:
Basic concepts
But the reason why immediate subscription is important to MobX (as opposed to a lifecycle hook that happens after render, such as useEffect) is because observables can be in ‘hot’ and ‘cold’ states (in RxJS terms). Suppose we have:
computed
) that computes the amount of unfinished computed itemsNow, the expression
computed
can be read in two different modes:MobX without concurrent mode
With that I mind, how
observer
was working so far is as follows:reaction
object which keeps track of all the dependencies being read during the render and subscribes to them. This caused the computed to become hot immediately, and a subscription to the computed to be set up. The computed in turn subscribes to the todos it used (and any other observable used). So a dependency graph forms.reaction
will no longer be GC-ed as long as the computed exists. Until the subscription is disposed. This is done automatically done duringcomponentWillUnmount
. This cause the reaction to release its subscriptions. And in turn, if nobody else depends on the ‘computed’ value, that will also release its subscriptions to the todos. Reaction objects can now be GC-ed.So what is the problem?
In concurrent mode, phase 2, disposing the reaction, might no longer happen, as an initial render can happen without matching “unmount” phase. (For just suspense promise throwing this seems avoidable by using
finally
)Possible solutions
Solution 1: Having an explicit hook that is called for never committed objects (e.g.
useOnCancelledComponent(() => reaction.dispose())
Solution 2: Have global state that records all created
reaction
objects, and remove reactions belonging to committed components from that collection. On a set interval dispose any non-committed reactions. This is the current (WIP) work around, that requires some magic numbers, such as what is a good grace period? Also needs special handling forreactions
that are cleaned up, but actually get committed after the grace period!Solution 3: Only subscribe after the component has been committed. This roughly looks like;
computed
value cold, meaning it will be untrackeduseEffect
) render another time, this time with proper tracking. This serves two goals:computed
between first rendering and commit will be picked up. (I can imagine this is a generic problem with subscribing to event emitters as part ofuseEffect
, updates might have been missed?)The downsides:
computed
will always compute twice, as there is first a ‘cold’, which won’t cache, and then a ‘hot’ read. Since in complicated applications there might be entire trees of depending computable values be needed, this problem might be worse than it sounds; as the dependencies of a computed value also go ‘cold’ if the computed value itself goes cold.Hope that explains the problem succinctly! If not, some pictures will be added 😃.