Skip to content

App Freezes on iOS When Dismissing Model After ColorPicker Selection #3784

@restarajat

Description

@restarajat

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions