Skip to content

feat: disable freeze for first render #1220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

kacperkapusciak
Copy link
Member

@kacperkapusciak kacperkapusciak commented Nov 24, 2021

Description

React-navigation's useIsFocused works based on changes between renders. Freezing stops all renders on non-visible screens thus breaking the useIsFocused hook.

This issue can be mitigated by allowing one more render before freezing the screen / disabling freeze for the first render.

This also fixes unnecessary pop animation on on stack reset.

Fixes #1198, #1209

Changes

  • Added DelayedFreeze component in index.native.tsx
  • Added e2e tests for bottom-tabs example

Screenshots / GIFs

#1198

Screen.Recording.2021-11-24.at.09.26.50.mov

#1209

Before

Simulator.Screen.Recording.-.iPhone.11.-.2021-11-24.at.10.59.38.mp4

After

Simulator.Screen.Recording.-.iPhone.11.-.2021-11-24.at.10.59.02.mp4

Test code and steps to reproduce

Test1198.tsx & Test1209.tsx in TestsExample/ project

Checklist

  • Included code example that can be used to test this change
  • Ensured that CI passes

@kacperkapusciak kacperkapusciak merged commit bf3d945 into master Dec 2, 2021
@kacperkapusciak kacperkapusciak deleted the @kacperkapusciak/disable-freeze-for-first-render branch December 2, 2021 10:17
@Aryk
Copy link

Aryk commented Nov 8, 2023

@kacperkapusciak I don't see the code from this merge in the current version:

https://github.com/software-mansion/react-native-screens/blob/3.27.0/src/index.native.tsx#L197

Or am I missing something?

I have version 3.22.0 which from June 2023 and I needed to use patch package and add the fix from here to get it to work:

#1198 (comment)

I can see 3.27 still has the old code.

kligarski added a commit that referenced this pull request Jun 9, 2025
…ite loop (#2963)

## 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 using `freezeOnBlur: 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 change `freeze` to `true`, we still pass previous freezeState
(`false`) to the `Freeze` component. `useEffect` runs and uses
`setImmediate` to update the `freezeState` right after the render. This
mechanism works for regular `push`/`card` screens but for some unknown
reason, it does not work with multiple screens with `presentation:
'modal'` and `headerShown: true` (more details below). Changing
`setImmediate` to `setInterval(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](https://github.com/facebook/react-native/blob/v0.79.3/packages/react-native/Libraries/Core/Timers/immediateShim.js#L44).
Microtasks run after a macrotask, before another interation of the event
loop. `setTimeout` is a regular macrotask. Somehow, when using
`setImmediate` in `DelayedFreeze`, after pushing 4 `modal` 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 multiple
`affectedLayoutableNodes`, they don't result in any mutations when
diffing the trees. These updates probably result in starvation of the
function passed to `setInterval`.

This bug happens with `presentation: 'formSheet'` as well but **not**
for other types of modals **including `presentation: 'pageSheet'`!**
This suggests that there is a problem with
`UIModalPresentationFormSheet` (to which `UIModalPresentationAutomatic`
usually resolves to since iOS 18) but not `UIModalPresentationPageSheet`
(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
|Screen|Presentation|Freeze|Details|
|------|------------|------|-------|
|Home |push |`false`| We enforce no freeze with `freezeOnBlur: false`|

##### Step 2
|Screen|Presentation|Freeze|Details|
|------|------------|------|-------|
|Home |push |`false`| We enforce no freeze with `freezeOnBlur: false`|
|Second|modal|`false`|The modal `isFocused` so we don't freeze it

##### Step 3
|Screen|Presentation|Freeze|Details|
|------|------------|------|-------|
|Home |push |`false`| We enforce no freeze with `freezeOnBlur: false`|
|Modal1|modal|`false`|The modal `isBelowFocused` so we don't freeze it
|Modal2|modal|`false`|The modal `isFocused` so we don't freeze it

##### Step 4
|Screen|Presentation|Freeze|Details|
|------|------------|------|-------|
|Home |push |`false`| We enforce no freeze with `freezeOnBlur: false`|
|Modal1|modal|`false`|The modal will be freezed after next render
(because of `DelayedFreeze`)
|Modal2|modal|`false`|The modal `isBelowFocused` so we don't freeze it
|Modal3|modal|`false`|The modal `isFocused` so we don't freeze it


##### Step 5
|Screen|Presentation|Freeze|Details|
|------|------------|------|-------|
|Home |push |`false`| We enforce no freeze with `freezeOnBlur: false`|
|Modal1|modal|`true`|
|Modal2|modal|`false`|The modal will be freezed after next render
(because of `DelayedFreeze`)
|Modal3|modal|`false`|The modal `isBelowFocused` so we don't freeze it
|Modal4|modal|`false`|The modal `isFocused` so we don't freeze it

This is the moment when infinite loop happens.

## Changes

- change `setImmediate` to `setTimeout(fn, 0)` in `DelayedFreeze`
- add `Test2963` test screen


## Screenshots / GIFs

### Before 

https://github.com/user-attachments/assets/c571e25a-d17f-4ae9-8bb6-06270ba6e1e9

### After 

https://github.com/user-attachments/assets/ebb461f5-9eef-4b50-8f41-473a38232cfd

## Test code and steps to reproduce

Open `Test2963` in example app.

## Checklist

- [x] Included code example that can be used to test this change
- [ ] Ensured that CI passes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants