Skip to content

Commit 8b52feb

Browse files
Extract applySynchronousUpdates from performOperations (#9078)
## Summary Separates the synchronous UI props update logic into its own `applySynchronousUpdates` method. Adds `partitionUpdates` and `shouldUseSynchronousUpdatesInPerformOperations` helpers to support the extracted method. ## Test plan
1 parent 114e1a5 commit 8b52feb

14 files changed

Lines changed: 398 additions & 40 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { useNavigation } from '@react-navigation/native';
2+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
3+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
4+
import React, { useCallback } from 'react';
5+
import { Button, StyleSheet, Text, View } from 'react-native';
6+
import Animated, {
7+
interpolate,
8+
useAnimatedScrollHandler,
9+
useAnimatedStyle,
10+
useSharedValue,
11+
} from 'react-native-reanimated';
12+
13+
type AndroidDrawPassParamList = {
14+
Description: undefined;
15+
ScrollList: undefined;
16+
};
17+
18+
const Stack = createNativeStackNavigator<AndroidDrawPassParamList>();
19+
const LIST_ITEM_COUNT = 80;
20+
21+
export default function AndroidDrawPassExample() {
22+
return (
23+
<View style={descriptionStyles.container}>
24+
<Stack.Navigator>
25+
<Stack.Screen name="Description" component={DescriptionScreen} />
26+
<Stack.Screen name="ScrollList" component={ScrollListScreen} />
27+
</Stack.Navigator>
28+
</View>
29+
);
30+
}
31+
32+
function DescriptionScreen() {
33+
const navigation =
34+
useNavigation<NativeStackNavigationProp<AndroidDrawPassParamList>>();
35+
36+
return (
37+
<View style={descriptionStyles.centered}>
38+
<Text style={descriptionStyles.title}>Android Draw Pass Fix</Text>
39+
<Text style={descriptionStyles.description}>
40+
This example reproduces a bug that was fixed on Android where committing
41+
updates during a draw pass caused a crash.
42+
</Text>
43+
<Text style={descriptionStyles.description}>
44+
Previously, if you navigated to the scroll list and went back{' '}
45+
<Text style={descriptionStyles.bold}>while actively scrolling</Text>,
46+
the app would crash. This happened because Reanimated was attempting to
47+
commit UI updates while Android was in the middle of a draw pass, and
48+
sometimes React would have some hierarchy mutations queued up that would
49+
get bundled with these updates, triggering the crash.
50+
</Text>
51+
<Text style={descriptionStyles.description}>
52+
The fix detects when a draw pass is in progress and pushes only
53+
non-layout updates through the synchronous update path, while layout
54+
updates are deferred to the next frame. This ensures that we never
55+
attempt to mutate the view hierarchy during a draw pass.
56+
</Text>
57+
<Button
58+
title="Go to scroll list"
59+
onPress={() => navigation.navigate('ScrollList')}
60+
/>
61+
</View>
62+
);
63+
}
64+
65+
function ListItem({ index }: { index: number }) {
66+
return (
67+
<View style={listStyles.listItem}>
68+
<Text>Item {index + 1}</Text>
69+
<Text style={listStyles.listItemSubtext}>
70+
Scroll fast and press Back — this used to crash on Android
71+
</Text>
72+
</View>
73+
);
74+
}
75+
76+
function ScrollListScreen() {
77+
const scrollY = useSharedValue(0);
78+
79+
const scrollHandler = useAnimatedScrollHandler({
80+
onScroll: (event) => {
81+
scrollY.value = event.contentOffset.y;
82+
},
83+
});
84+
85+
const floatingBadgeStyle = useAnimatedStyle(() => {
86+
const scale = interpolate(scrollY.value, [0, 100, 200], [1, 1.2, 0.9]);
87+
const rotate = interpolate(scrollY.value, [0, 150, 300], [0, 5, -5]);
88+
89+
return {
90+
width: 100,
91+
transform: [{ scale }, { rotate: `${rotate}deg` }],
92+
};
93+
});
94+
95+
const renderItem = useCallback(
96+
({ item }: { item: number }) => <ListItem index={item} />,
97+
[]
98+
);
99+
100+
const keyExtractor = useCallback((item: number) => `item-${item}`, []);
101+
const data = Array.from({ length: LIST_ITEM_COUNT }, (_, index) => index);
102+
103+
return (
104+
<View style={listStyles.container}>
105+
<Animated.View style={[listStyles.floatingBadge, floatingBadgeStyle]}>
106+
<Text style={listStyles.badgeText}>scrollY</Text>
107+
</Animated.View>
108+
109+
<Animated.FlatList
110+
data={data}
111+
renderItem={renderItem}
112+
keyExtractor={keyExtractor}
113+
onScroll={scrollHandler}
114+
scrollEventThrottle={16}
115+
contentContainerStyle={listStyles.listContent}
116+
showsVerticalScrollIndicator
117+
/>
118+
</View>
119+
);
120+
}
121+
122+
const descriptionStyles = StyleSheet.create({
123+
container: {
124+
flex: 1,
125+
},
126+
centered: {
127+
flex: 1,
128+
alignItems: 'center',
129+
gap: 16,
130+
padding: 24,
131+
},
132+
title: {
133+
fontSize: 20,
134+
fontWeight: '700',
135+
marginBottom: 4,
136+
},
137+
description: {
138+
color: '#333',
139+
fontSize: 15,
140+
lineHeight: 22,
141+
},
142+
bold: {
143+
fontWeight: '700',
144+
},
145+
});
146+
147+
const listStyles = StyleSheet.create({
148+
container: {
149+
flex: 1,
150+
},
151+
floatingBadge: {
152+
position: 'absolute',
153+
top: 100,
154+
right: 20,
155+
zIndex: 10,
156+
paddingVertical: 8,
157+
paddingHorizontal: 14,
158+
borderRadius: 12,
159+
backgroundColor: 'rgba(255, 149, 0, 0.9)',
160+
},
161+
badgeText: {
162+
color: '#fff',
163+
fontWeight: '600',
164+
},
165+
listContent: {
166+
paddingTop: 24,
167+
paddingBottom: 40,
168+
paddingHorizontal: 20,
169+
},
170+
listItem: {
171+
marginBottom: 8,
172+
borderRadius: 8,
173+
paddingVertical: 16,
174+
paddingHorizontal: 12,
175+
backgroundColor: 'rgba(128, 128, 128, 0.12)',
176+
},
177+
listItemSubtext: {
178+
marginTop: 6,
179+
opacity: 0.8,
180+
},
181+
});

apps/common-app/src/apps/reanimated/examples/StickyHeaderExample.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ export default function StickyHeaderExample() {
2424
});
2525

2626
const animatedStyle = useAnimatedStyle(() => {
27-
return { transform: [{ translateY: offset.value }] };
27+
return {
28+
transform: [{ translateY: offset.value }],
29+
width: (offset.value % 200) + 100,
30+
};
2831
});
2932

3033
return (

apps/common-app/src/apps/reanimated/examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ import WorkletFactoryCrash from './WorkletFactoryCrashExample';
160160
import WorkletRuntimeExample from './WorkletRuntimeExample';
161161
import InstanceDiscoveryExample from './InstanceDiscoveryExample';
162162
import ShadowNodesCloningExample from './ShadowNodesCloningExample';
163+
import AndroidDrawPassExample from './AndroidDrawPassExample';
163164

164165
export const REAPlatform = {
165166
IOS: 'ios',
@@ -204,6 +205,11 @@ export const EXAMPLES: Record<string, Example> = {
204205
title: 'Sync back to React',
205206
screen: SyncBackToReactExample,
206207
},
208+
AndroidDrawPassExample: {
209+
icon: '✍️',
210+
title: 'Android Draw Pass',
211+
screen: AndroidDrawPassExample,
212+
},
207213
DetachAnimatedStylesExample: {
208214
icon: '⛓️‍💥',
209215
title: 'Detach animated styles',

apps/fabric-example/ios/Podfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,7 +2519,7 @@ EXTERNAL SOURCES:
25192519

25202520
SPEC CHECKSUMS:
25212521
FBLazyVector: c12d2108050e27952983d565a232f6f7b1ad5e69
2522-
hermes-engine: 53012435cc80627d41ad53d8c27ecc7efaef2f12
2522+
hermes-engine: f111e1a9cf7d7933bdceaf272d1777d0fb1d89ac
25232523
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
25242524
NitroMmkv: 7fe66a61d5acab6516098a64f42af575595e7566
25252525
NitroModules: 70861471ee1cc5616462aef869a164dac12db7b9
@@ -2531,7 +2531,7 @@ SPEC CHECKSUMS:
25312531
React: 7ef36630d07638043a134a7dd2ec17e0be10fc3c
25322532
React-callinvoker: af4e8fe1d60ab63dd8d74c2a68988064c2848954
25332533
React-Core: c609976c034ba9556bef9850a571a71bd458d73f
2534-
React-Core-prebuilt: ea396d0e40644e9e46245e9b4b6db931b6be006b
2534+
React-Core-prebuilt: 71485c8868eb75b30e2f564f19187f96b53403ee
25352535
React-CoreModules: 0ea85f3b3f4b8cbfb3afacd2ed85458fb878517a
25362536
React-cxxreact: 6752bab77c0599d6136e2b8b9b64b4a7d316d401
25372537
React-debug: 38389b86e3570558ec73dd4cbc0cd2f2eec47a51
@@ -2594,7 +2594,7 @@ SPEC CHECKSUMS:
25942594
ReactAppDependencyProvider: 6b7e8d8d974ed13fb66698d82c30c5e70c1f7d3a
25952595
ReactCodegen: 2fe81bae8e7b638d8d9b78c3dc1ccc1eeabfc613
25962596
ReactCommon: 92b53b0bd7f7d86154dc9f512c1ea5dee717cc72
2597-
ReactNativeDependencies: 554aa4807a73d8b65ad8147af7b2d6042b7787be
2597+
ReactNativeDependencies: 5cedfcfef19b6597a3c9b4bfc42de85a7b3d39cc
25982598
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
25992599
RNCClipboard: 88d7eeb555d1183915f0885bdbc5c97eb6f7f3ba
26002600
RNCMaskedView: d2578d41c59b936db122b2798ba37e4722d21035

packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ void UpdatesRegistry::flushUpdates(UpdatesBatch &updatesBatch) {
3838
}
3939
}
4040

41+
UpdatesBatch UpdatesRegistry::getPendingUpdates() {
42+
auto lock = std::lock_guard<std::mutex>{mutex_};
43+
flushUpdatesToRegistry(updatesBatch_);
44+
45+
UpdatesBatch updatesBatch;
46+
for (const auto &[tag, pair] : updatesRegistry_) {
47+
const auto &[shadowNode, props] = pair;
48+
updatesBatch.emplace_back(shadowNode, props);
49+
}
50+
return updatesBatch;
51+
}
52+
4153
void UpdatesRegistry::collectProps(PropsMap &propsMap) {
4254
std::lock_guard<std::mutex> lock{mutex_};
4355

packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/UpdatesRegistry.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class UpdatesRegistry {
4646

4747
void flushUpdates(UpdatesBatch &updatesBatch);
4848
void collectProps(PropsMap &propsMap);
49+
UpdatesBatch getPendingUpdates();
4950

5051
protected:
5152
mutable std::mutex mutex_;

0 commit comments

Comments
 (0)