Open jeffijoe opened 7 months ago
Eh... that test description definitely explains issues I've seen around automation with other projects in the past, even not MobX related 😅
curious if #3863 has an effect on this
If you can release it for preview I can give it a shot!
Hi @mweststrate my PR won't have any effect on this as the PR is for class component only. for this issue, I spent some time to investigate. I believe this is not a mobx-react-lite issue, thus I suggest this issue should be closed
pendingStateVersion
, so that we prevent calling onStoreChange
multiple times before re-render, in theory, one onStoreChange
call is enough to let react to schedule the update. However this doesn't fix the issueonSelectedItemChange
from downshift
is called during commit phase, essentially what happened in this example is
user clicks -> onSelectedItemChange
called during commit phase -> mutate the observable store -> useObserver's reaction runs -> onStoreChange()
so react schedule an update in SyncLane -> react renders this component, we now consider the component is up-to-date -> in the commit phase, downshift
calls onSelectedItemChange again -> mutate the observable store -> useObserver's reaction runs -> ...... loopThe problem here is downshift
calls onSelectedItemChange again at commit phase. mobx-react-lite does everything correctly
cc @jeffijoe
@zhantx for context, when using regular React state (without mobx) in the Downshift reproduction, it only runs twice instead of looping, that's why I opened an issue here as well.
That's right, let me explain When a component needs to update (i.e. setState from useState hook is called), react schedule/enqueue an update then, react starts a render cycle, which consists of render phase (react calls render function) and commit phase (react commit DOM, react calls effects)
for regular React state
onSelectedItemChange
in commit phasewhen using mobx, because mobx is an external store, naturally, it uses useSyncExternalStore
hook
onSelectedItemChange
in commit phaseIt seems to me that mobx-react is doing what we expect, that if any store mutates after render phase, we should let react to re-render the component. and because mobx is an external store, any updates should be in SyncLane This is more background on useSyncExternalStore and why mobx-react should use it
Intended outcome:
There should be no
Maximum update depth exceeded
errorActual outcome:
React throws a
Maximum update depth exceeded
error.How to reproduce the issue:
I created a reproduction repository (see
src/MobX.jsx
) - it includes bothmobx
anddownshift
, this is becausedownshift
is the root cause however the waymobx-react
behaves after version 8 (with the change touseSyncExternalStore
) makes me believe it's worth investigating on themobx-react
side.You'll be able to see for yourself by downgrading to
mobx-react@7
in that repo and running the test - it will show2
in the counter instead of53
(at which point the error is thrown which stops the loop).I know the issue template states to avoid speculation but I've spent way too long investigating what's going on to not not mention it 😅 What seems to be happening is the
useSyncExternalStore
will "push" another work item on the internal React sync queue (for batching) and because every reaction will makemobx-react
's implementation return a new symbol ingetSnapshot
, if React triggers an effect which subsequently triggers a MobX reaction, it will keep pushing to that queue for each iteration.https://github.com/mobxjs/mobx/blob/13a74fe43de683249b2fd17145957b78f5befee4/packages/mobx-react-lite/src/useObserver.ts#L49-L72
New work pushed to queue
As I mentioned, the root cause is a different library, but what prompted me to look into this was because in my real application, the browser tap freezes when this happens as the error actually loops infinitely there. I couldn't reproduce that here though. Additionally, when using plain React
useState
(as seen inBasic.jsx
), it does not result in the error.I have also opened an issue with
downshift
.Versions
mobx@6.12.3
mobx-react@9.1.1
react@18.2.0
react-dom@18.2.0