fix: schedule delayed freeze with setTimeout
to prevent React infinite loop
#2963
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Description
When analyzing Test791 in order to create an e2e test for it, I noticed some weird behavior that is probably caused by multiple overlapping issues. This PR addresses one of them - when pushing modals with a header in
setInterval
from a regular push screen that is not freezed (we prevent freezing it by usingfreezeOnBlur: false
option), only 4 of them are pushed. Then the interval stops and the screens are not being pushed anymore. What is even more interesting, interacting with a button in the modal fixes the issue, sometimes only for one more push, but sometimes it just starts working without any issues.DelayedFreeze component was introduced in #1220 in order to fix react-navigation's
useIsFocused
hook and some animation bugs. DelayedFreeze allows one more render before freezing the screen. If we changefreeze
totrue
, we still pass previous freezeState (false
) to theFreeze
component.useEffect
runs and usessetImmediate
to update thefreezeState
right after the render. This mechanism works for regularpush
/card
screens but for some unknown reason, it does not work with multiple screens withpresentation: 'modal'
andheaderShown: true
(more details below). ChangingsetImmediate
tosetInterval(fn, 0)
fixes the issue.EDIT: Seems like the issue happens also in different scenarios with freeze (e.g. #2971). The bug is present only for RN 0.78+, it might be related to React 19.
Fixes #2971.
Details
setImmediate
creates a microtask. Microtasks run after a macrotask, before another interation of the event loop.setTimeout
is a regular macrotask. Somehow, when usingsetImmediate
inDelayedFreeze
, after pushing 4modal
screens with a header, when the first modal gets freezed, an infinite loop of commits is created (you can see the CPU usage go to max, allocated memory increases). Even though each commit has multipleaffectedLayoutableNodes
, they don't result in any mutations when diffing the trees. These updates probably result in starvation of the function passed tosetInterval
.This bug happens with
presentation: 'formSheet'
as well but not for other types of modals includingpresentation: 'pageSheet'
! This suggests that there is a problem withUIModalPresentationFormSheet
(to whichUIModalPresentationAutomatic
usually resolves to since iOS 18) but notUIModalPresentationPageSheet
(which was the default before iOS 18).Bug mechanism described step-by-step
Please note that react-navigation freezes currently focused screen and one below (this is necessary for animations).
Step 1
false
freezeOnBlur: false
Step 2
false
freezeOnBlur: false
false
isFocused
so we don't freeze itStep 3
false
freezeOnBlur: false
false
isBelowFocused
so we don't freeze itfalse
isFocused
so we don't freeze itStep 4
false
freezeOnBlur: false
false
DelayedFreeze
)false
isBelowFocused
so we don't freeze itfalse
isFocused
so we don't freeze itStep 5
false
freezeOnBlur: false
true
false
DelayedFreeze
)false
isBelowFocused
so we don't freeze itfalse
isFocused
so we don't freeze itThis is the moment when infinite loop happens.
Changes
setImmediate
tosetTimeout(fn, 0)
inDelayedFreeze
Test2963
test screenScreenshots / GIFs
Before
before_2963_fhd.mov
After
after_2963_fhd.mov
Test code and steps to reproduce
Open
Test2963
in example app.Checklist