fix(mobx-react-lit): dispose reactions right after render in StrictMode and Suspense#3777
fix(mobx-react-lit): dispose reactions right after render in StrictMode and Suspense#3777Wroud wants to merge 11 commits into
Conversation
…de and Suspense onBecomeUnobserved never triggered with observer component mobxjs#3774
|
|
Few quick notes without giving it proper look: |
|
I see your point. In this PR useLayoutEffect is used only for:
I tested this implementation on the huge project with use of Suspense and haven't noticed any issues. But it seems that this implementation works the same way in the most cases. |
|
I will try to move logic from |
|
This implementation doesn't re-render components if the reaction has been disposed of. How it works:
I've tested this implementation with extra logs, and there are no extra re-renders |
|
@urugator I also added some extra tests for abandoned and suspended components to ensure that reactions are disposed of correctly |
|
Are you aware of the It's the same idea, but doesn't create timeout for each component and is a bit less aggresive with disposal. It's intended as a poor men's GC. The ability to rely on actual GC is considered a feature here. |
|
Yes, it's the same idea as There is a small difference between the original implementation and this one; despite using Are there any blockers to use |
|
Here is an example: The easiest way here is to load data in the background if data is observed. With GC, data will be kept up to date for a long time, even if it's no longer used. |
Atm
I do understand the concern, but the current solution is deliberate. It's not about providing an implementation (while appreciated), but about changing some design decision. cc @mweststrate: |
|
idea: we can probably use
so when we dispose of a reaction because of timeout, we can also change EDIT: Maybe it's only works for transactions |
That's what we do: mobx/packages/mobx-react-lite/src/useObserver.ts Lines 55 to 64 in 273e017 |
|
It's equal to run it in |
|
in that case component will try to render 2 times, but the actual render will be the only once, in case we change the state in effect's phase component will be committed and then will start a new render |
|
I see. So, potentially, we can move to and get rid of because that should never happen (react throws away uncommited component, because it sees a new version) ?? |
|
Yes, that's exactly what I mean |
|
With shorthand implementation, I can notice this warning in the tests: but I call |
|
If it works, I am personally open to that change. But it doesn't help much with the original issue or does it?
If these are from the deprecated
Where? In finalizer? |
test("sync state access count", async () => {
let snapshotAccess = 0
function sub() {
return () => {}
}
function getStateSnapshot() {
snapshotAccess++
return 0
}
const TestComponent = () => {
React.useSyncExternalStore(sub, getStateSnapshot)
return <>0</>
}
render(<TestComponent />)
expect(snapshotAccess).toBe(2)
})Output quite surprised me: |
|
there new two from |
|
I've pushed these small changes, I haven't noticed any This won't fix the original issue (dispose of reaction right when the component is mounted or abandoned) |
Thoughts why it's three but not 2:
|
|
Sorry no time atm to think this one through, I'll try to look later this
week / next weekend
…On Sun, 22 Oct 2023, 13:46 Alexey, ***@***.***> wrote:
test("sync state access count", async () => {
let snapshotAccess = 0
function sub() {
return () => {}
}
function getStateSnapshot() {
snapshotAccess++
return 0
}
const TestComponent = () => {
React.useSyncExternalStore(sub, getStateSnapshot)
return <>0</>
}
render(<TestComponent />)
expect(snapshotAccess).toBe(2)})
Output quite surprised me:
● sync state access count
expect(received).toBe(expected) // Object.is equality
Expected: 2
Received: 3
220 | render(<TestComponent />)
221 |
> 222 | expect(snapshotAccess).toBe(2)
| ^
223 | })
224 |
Thoughts why it's three but not 2:
1. The first one is when we render the component
2. second one before we ready to commit it
3. third one after we subscribed to store (effect phase)
—
Reply to this email directly, view it on GitHub
<#3777 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAN4NBAKDVQVTLBDOPSTMMLYAUBQTAVCNFSM6AAAAAA6HTXGZCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTONZUGA3TCOBVHE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
|
Apologies, just found this PR, totally lost track of it. Do we feel this still worth pursuing? I lost track in the convo above what it does and doesn't improve :). |
|
the main goal here is to remove subscription to observer immediatly if component rendering abandoned. This will fix onBecomeObserved/onBecomeUnobserved callbacks in some cases (current implementation based on GC and have a problem with long living subscriptions) |
|
I found this PR linked to this issue as I've been debugging why I tried with and without React |
|
I was reading on
Considering this, I personally think the changes in this PR make sense. Perhaps we could even explore whether using React's scheduler is possible to avoid the use of EDIT: What about a hybrid approach that uses |
|
This is quite important because it means that composing the use of react suspense and higher level mobx constructs like |
|
@mweststrate @jeffijoe Would it be possible to revisit this work, we are having a lot of issues where |
|
@Wroud FYI I have found an issue with this solution, namely it doesn't work ( |
|
Annoyingly the following code seems to always work (as a replacement for calling export const useTracked = <T>(computed: IComputedValue<T>) => {
const subscribe = useCallback((cb: VoidFunction) => autorun(() => (computed.get(), cb())), [computed]);
const getSnapshot = useCallback(() => untracked(() => computed.get()), [computed]);
return useSyncExternalStore(subscribe, getSnapshot);
};And this code seems to always correctly report |
|
Unfortunately after several days of trying I was unable to produce a solution that satisfied all of my unit tests and requirements along with the original unit tests of maintaining observers across suspense points. Furthermore, I was only able to do this by using a |
|
@Wroud @jeffijoe I have updated my PR #4609 with what seems to be a working solution, it does cause additional This fixes all the unit tests, but I am still observing errors in my production code. I will get back to you if I can figure out why reliably PTAL |
|
@Nokel81 just for the record, I'm not a maintainer, just a long-time user 😅 But I am subscribed to this either way! Thanks for your effort in getting this resolved, but we are still at the mercy of @urugator or @mweststrate to get any potential fix released. 🙏 |
|
Understood, also I have found in production that I do actually still need the 100ms timeout. For some reason react has some intermediate renders that cause It does satisfactorily work, but it is technically a race condition. So it only ensures that the unobservation happens on unmount if the unmount is >100ms after the last suspense |
|
@mweststrate @urugator has there been any recent revelations on this matter? The impact of this is quite considerable, as it makes MobX look incompatible with an integral functionality of React (ie. Still: Thanks for all the other awesome stuff so far :) |
This pull request replaces
observerFinalizationRegistrywith the combination ofuseLayoutEffectwithrequestAnimationFrameThe benefit is simple: reactions are disposed of right after the component is unmounted or thrown an exception or in case of react suspense or strict mode.
Related issue: #3774