react-native-ui-lib icon indicating copy to clipboard operation
react-native-ui-lib copied to clipboard

App Freezes on iOS When Dismissing Model After ColorPicker Selection

Open restarajat opened this issue 5 months ago • 1 comments

Component: Model, ColorPicker Platform: iOS React Native version: 0.80.1 react-native-ui-lib: 7.44.0 react-native-gesture-handler: 2.27.2 react-native-reanimated: 3.19.0

Description

When using the BottomSheet (Build using Model component) component to display a color picker, the app sometimes freezes or crashes on iOS after selecting a color and dismissing the bottom sheet. The following error appears in the logs:

I0723 10:12:31.732117 1842884608 UIManagerBinding.cpp:135] instanceHandle is null, event of type topMomentumScrollEnd will be dropped

Related to

  • Components

Steps to reproduce

Steps to reproduce the behaviour:

  1. Open the color picker (inside a BottomSheet).
  2. Select a color.
  3. The app may freeze or crash, and the above error appears in the logs.

Expected behavior

  • The bottom sheet should close smoothly after a color is selected, regardless of scroll state.
  • The app should not freeze or crash.

Actual behaviour

  • On iOS, the app sometimes freezes or crashes if the bottom sheet is dismissed while a scroll/momentum event is still in progress.
  • The error instanceHandle is null, event of type topMomentumScrollEnd will be dropped appears in the logs.

More Info

Code snippet

BottomSheet.tsx

import { useState, useRef, useImperativeHandle, type RefObject, type Ref, type ReactElement, type ReactNode } from 'react';
import { Dimensions, ScrollView, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { View, Text, Colors, Modal } from 'react-native-ui-lib';

type BottomSheetData = string | number | object | object[] | string[] | null | undefined;

export interface BottomSheetRef {
	data: RefObject<unknown>;
	showBottomSheet: (popupData?: BottomSheetData) => void;
	hideBottomSheet: () => void;
}

interface BottomSheetProps {
	isClosable?: boolean;
	isScrollEnabled?: boolean;
	hasCloseButton?: boolean;
	isFullScreen?: boolean;
	title?: string;
	subTitle?: string;
	wrapperHeight?: number;
	top?: number;
	ref: Ref<BottomSheetRef>;
	children: ReactNode;
	renderHeader?: () => ReactNode;
	renderFooter?: () => ReactNode;
}


function BottomSheet({ children,
	ref,
	isScrollEnabled = false,
	isClosable = true,
	isFullScreen = false,
	title = '',
	subTitle = '',
	wrapperHeight = undefined,
	top = undefined,
	renderHeader = undefined,
	renderFooter = undefined }: BottomSheetProps): ReactElement {
	// Variables
	const { height } = Dimensions.get('window');

	// States initialization.
	const [isBottomSheetOpen, setBottomSheetOpen] = useState(false);

	// Stateless variable(ref).
	const data = useRef<BottomSheetData>(null);

	/** Imperative handler to declare the accessible function inside components using refs. */
	useImperativeHandle(ref, () => ({
		showBottomSheet,
		hideBottomSheet,
		data
	}));

	function showBottomSheet(popupData?: BottomSheetData): void {
		data.current = popupData;
		setBottomSheetOpen(true);
	}

	function hideBottomSheet(): void {
		if (isClosable) {
			data.current = null;
			setBottomSheetOpen(false);
		}
	}

	function renderTitle(): ReactElement | null {
		if (title) {
			return (
				<View padding-20>
					<View row>
						<Text h3>{title}</Text>
					</View>
					{subTitle ? <Text p marginT-5>{subTitle}</Text> : null}
				</View>
			);
		}
		return null;
	}

	function renderScrollView(): ReactElement {
		if (!isScrollEnabled) {
			return (
				<View height={wrapperHeight}>
					{children}
				</View>
			);
		}
		return (
			<ScrollView
				showsVerticalScrollIndicator={false}
				keyboardDismissMode="interactive"
				keyboardShouldPersistTaps="always"
			>
				{children}
			</ScrollView>
		);
	}

	function renderBottomSheetBody(): ReactElement {
		const regMaxheight = height - (top || 150);
		return (
			<View bottom flex>
				<View centerH paddingV-15>
					<View bg-opacityLight br100 width={80} height={7} />
				</View>
				<View
					bg-white
					useSafeArea
					style={[{
						maxHeight: isFullScreen ? '100%' : regMaxheight,
						borderTopLeftRadius: 30,
						borderTopRightRadius: 30
					}]}
				>
					{renderHeader?.() ?? renderTitle()}
					{renderScrollView()}
					{renderFooter?.()}
				</View>
			</View>
		);
	}

	return (
		<GestureHandlerRootView style={styles.wrapper}>
			<Modal
				useKeyboardAvoidingView
				navigationBarTranslucent
				statusBarTranslucent
				transparent
				visible={isBottomSheetOpen}
				animationType="slide"
				overlayBackgroundColor={Colors.backdrop}
				onRequestClose={hideBottomSheet}
				onBackgroundPress={hideBottomSheet}
				onDismiss={hideBottomSheet}
			>
				{renderBottomSheetBody()}
			</Modal>
		</GestureHandlerRootView>
	);
}

const styles = StyleSheet.create({
	wrapper: {
		flex: 0,
		zIndex: 9999
	},
});

export default BottomSheet;

Toolbar.tsx

import React, { useRef } from 'react'
import { Button, ColorPicker, Colors, Text, View } from 'react-native-ui-lib';
import BottomSheet, { BottomSheetRef } from './BottomSheet';

function Toolbar({ onChange }: { onChange: (value: string) => void }) {
   const bottomSheetRef = useRef<BottomSheetRef>(null);

   const handleChange = (value: string) => {
      onChange(value);
      bottomSheetRef.current?.hideBottomSheet();
   }

   return (
      <View>
         <Text>Toolbar</Text>
         <Button label="Change Color" onPress={() => bottomSheetRef.current?.showBottomSheet()} />
         <BottomSheet ref={bottomSheetRef}>
            <View padding-30>
               <ColorPicker initialColor={Colors.blue1} colors={[]} onSubmit={handleChange} />
            </View>
         </BottomSheet>
      </View>
   )
}

export default Toolbar;

App.tsx

import { StatusBar, StyleSheet, useColorScheme } from 'react-native';
import {  useState } from 'react';
import { View, Text, Colors } from 'react-native-ui-lib';
import Toolbar from './Toolbar';

function App() {
  const isDarkMode = useColorScheme() === 'dark';
  const [activeColor, setActiveColor] = useState(Colors.blue1);

  return (
    <View useSafeArea backgroundColor={Colors.white} style={styles.container}>
      <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
      <Text>Active Color: {activeColor}</Text>
      <View style={{backgroundColor: activeColor, width: 100, height: 100}} />
      <Toolbar onChange={setActiveColor} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

export default App;

Workaround: delay hiding the bottom sheet to avoid crash/hang on iOS

 const handleChange = (value: string) => {
      onChange(value);
      setTimeout(() => {
         bottomSheetRef.current?.hideBottomSheet();
      }, 500);
   }

Environment

  • React Native version: 0.80.1
  • React Native UI Lib version: 7.44.0

Affected platforms

  • iOS

restarajat avatar Jul 23 '25 06:07 restarajat

Hello,

We have a version that supports new-arch (RN77), you can use the next tag for now. Please make sure to go over the v8 doc, it includes breaking changes and some known issues.

Please close this ticket if it solved your bug.

M-i-k-e-l avatar Oct 23 '25 11:10 M-i-k-e-l