Issue: Gray screen appears on Android when users enable "Reduce Motion" accessibility setting and quickly swipe between channels.
Root Cause: Animation updates (translateX) arrive while components are frozen/unregistered, causing updates to be lost and components to render with stale values.
Context: I've been working on this issue and there are currently three different solutions in play. I want to present the situation to the team and get alignment on the best approach.
- User enables reduced motion → Animations complete instantly
- User swipes to close channel →
translateXbecomes411px(off-screen) - Component freezes →
ComponentRegistry.unregister()removes component - User immediately swipes to open →
translateXupdates to0 - Update arrives while frozen →
ComponentRegistry.getComponent()returnsundefined - Update is lost → No way to apply
translateX: 0 - Component unfreezes → Renders with old
translateX: 411px(gray screen)
Link Here Implementation: Cache updates when components are unregistered, apply on re-registration.
// ComponentRegistry.ts
const pendingUpdates = new Map<number | HTMLElement, any>();
register: (tag, component) => {
componentRegistry.set(tag, component);
const pendingUpdate = pendingUpdates.get(tag);
if (pendingUpdate) {
component._updateReanimatedProps(pendingUpdate);
pendingUpdates.delete(tag);
}
},
cacheUpdate: (tag, props) => {
pendingUpdates.set(tag, props);
}
// updateProps.ts
function updatePropsOnReactJS(tag: number, props: StyleProps) {
const component = ComponentRegistry.getComponent(tag);
if (component) {
component._updateReanimatedProps(props);
} else {
ComponentRegistry.cacheUpdate(tag, props); // Cache when component not found
}
}Pros:
- ✅ Fixes root cause (lost updates during freeze)
- ✅ Clean, targeted solution
- ✅ No conflicts with Reanimated v4 migration
Cons:
- ❌ Requires maintaining fork of Reanimated
- ❌ Currently commented out in our codebase (needs to be uncommented)
Link here Implementation: Check if styles are actually attached before skipping re-registration.
// ViewDescriptorsSet.ts
const viewTags = new Set<number>();
has: (viewTag: number) => viewTags.has(viewTag),
// createAnimatedComponent.tsx
const isStyleAttached = (style: StyleProps) =>
style.viewDescriptors.has(viewTag);
if (hasOneSameStyle && isStyleAttached(prevStyles[0])) {
return; // Only skip if already attached
}Why it doesn't work for our case:
- ❌ Assumes component exists to receive updates
- ❌ Our problem: component doesn't exist when updates arrive
- ❌ Addresses different issue (style re-registration vs lost updates)
Link Here Implementation: Force style recomputation after unfreeze with timeout.
// MainTabsChannelScreenStack.tsx
const freezeValue = useSharedValue(0);
React.useEffect(() => {
const timeout = setTimeout(() => {
freezeValue.set(freezeValue.get() + 1);
}, 70);
}, [freeze, freezeValue]);
// useMainTabsChannelScreenStyles.tsx
const animatedStyles = useAnimatedStyle(() => {
freezeValue?.get(); // Force recomputation
return { transform: [{translateX: translateX.get()}] };
});Pros:
- ✅ Works (fixes gray screen)
- ✅ No Reanimated fork needed
Cons:
- ❌ Causes flicker (10ms delay) where the screen is still invisible, and then suddenly appears
- ❌ Workaround, not root cause fix
- ❌ May be redundant with cached updates solution (needs verification)
The Reanimated team's fix addresses style re-registration after freeze/unfreeze cycles. Our issue is fundamentally different:
- Their problem: Style object exists but isn't attached to view
- Our problem: Style updates arrive when component doesn't exist at all