question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Concurrent] Safely disposing uncommitted objects

See original GitHub issue

How 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:open
  • Created 4 years ago
  • Reactions:17
  • Comments:77 (22 by maintainers)

github_iconTop GitHub Comments

38reactions
gaearoncommented, Oct 25, 2020

But as mentioned above, people are making other libraries that are activated by a React.FC being called and there being cases when the FC is called and no cleanup opportunity is presented.

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.

20reactions
mweststratecommented, Apr 10, 2019

@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:

  1. A collection of todo items
  2. An expression (we call this a computed) that computes the amount of unfinished computed items
  3. A component that renders the amount of unfinished computed items.

Now, the expression computed can be read in two different modes:

  1. If the computed is cold, that is, no one is subscribed to it’s result; it evaluates as a normal expression, returning and forgetting its result
  2. If the computed is hot, it will start observing its own dependencies (the todo items), and caching the response as long as the todo item set is unaffected and the output of the computed expression is unchanged.

MobX without concurrent mode

With that I mind, how observer was working so far is as follows:

  1. During the first (and any subsequent) render, MobX creates a so called 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.
  2. Since there is an immediate subscription, it means that the reaction will no longer be GC-ed as long as the computed exists. Until the subscription is disposed. This is done automatically done during componentWillUnmount. 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 for reactions 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;

  1. During the first render, read the computed value cold, meaning it will be untracked
  2. After commit (using useEffect) render another time, this time with proper tracking. This serves two goals:
  3. The dependencies can be established
  4. Any updates to the 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 of useEffect, updates might have been missed?)

The downsides:

  1. Every component will render twice!
  2. The 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 😃.

Read more comments on GitHub >

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

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found