Skip to content

iOS native crash when rapidly toggling multiple ControlField + Checkbox items in a list #426

@DigitalAndSEO

Description

@DigitalAndSEO

Before submitting a new issue

  • I tested using the latest version of the library, as the bug might be already fixed.
  • I tested using a supported version of react native.
  • I checked for possible duplicate issues, with possible answers.

Bug summary

Summary

We are seeing an intermittent native iOS crash when rendering a list of selectable items using HeroUI Native ControlField + Checkbox, especially when several rows update selection/disabled state quickly.

The crash does not surface as a JavaScript error in Metro. The app process aborts natively and iOS crash reports consistently point to React Native Fabric view unmounting:

-[RCTViewComponentView unmountChildComponentView:index:]
RCTPerformMountInstructions(...)
-[RCTMountingManager performTransaction:]

After replacing the HeroUI Native ControlField/Checkbox rows with plain React Native Pressable rows and a very simple View-based custom checkbox indicator, the crash appears to stop.

Environment

heroui-native: 1.0.3
expo: 55.0.26
expo-dev-client: 55.0.35
react: 19.2.0
react-native: 0.83.6
react-native-reanimated: 4.2.1
react-native-svg: 15.15.3
react-native-worklets: 0.7.4
react-native-screens: 4.23.0
react-native-gesture-handler: 2.30.0
node: 24.16.0
pnpm: 11.1.2

Device/runtime:

Platform: iOS physical device
iOS version from crash report: iPhone OS 26.4.2
Build type: Expo dev client / TestFlight-installed dev build

What we were rendering

A scrollable list of rows, each row roughly like this:

<ControlField
  isSelected={isSelected}
  isDisabled={isDisabled}
  onSelectedChange={(nextSelected) => {
    toggleItem(item.id, nextSelected);
  }}
>
  <ControlField.Indicator>
    <Checkbox />
  </ControlField.Indicator>

  <View>
    <Text>{item.label}</Text>
    <Text>{item.description}</Text>
  </View>
</ControlField>

The list had around 6 items.

The relevant behavior:

  • User can select up to N items.
  • Once N items are selected, the remaining unselected rows become disabled.
  • If the user unselects one item, disabled rows become enabled again.
  • There is also a separate acknowledgement checkbox below the list.
  • Rapidly toggling rows, selecting/unselecting, and changing disabled state could crash the app.

Steps to reproduce

A minimal reproduction would likely be:

  1. Render a scrollable list of multiple ControlField rows.
  2. Each row contains ControlField.Indicator with Checkbox.
  3. Keep selected IDs in React state.
  4. Dynamically set each row as disabled when a selection limit is reached.
  5. Rapidly tap rows:
    • select item A
    • select item B
    • unselect item A
    • select item C
    • repeat quickly
  6. On iOS physical device, the app sometimes aborts natively.

Example logic:

const [selectedIds, setSelectedIds] = useState<string[]>([]);
const limit = 2;

return (
  <ScrollView>
    {items.map((item) => {
      const isSelected = selectedIds.includes(item.id);
      const isDisabled = !isSelected && selectedIds.length >= limit;

      return (
        <ControlField
          key={item.id}
          isSelected={isSelected}
          isDisabled={isDisabled}
          onSelectedChange={(nextSelected) => {
            setSelectedIds((current) => {
              if (nextSelected) return [...current, item.id];
              return current.filter((id) => id !== item.id);
            });
          }}
        >
          <ControlField.Indicator>
            <Checkbox />
          </ControlField.Indicator>

          <View>
            <Text>{item.label}</Text>
          </View>
        </ControlField>
      );
    })}
  </ScrollView>
);

Actual result

The app crashes natively. No useful JS error is shown in Metro.

Crash report excerpt:

Exception Type: EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6

Last Exception Backtrace:
__exceptionPreprocess
objc_exception_throw
-[NSAssertionHandler handleFailureInFunction:file:lineNumber:description:]
-[RCTViewComponentView unmountChildComponentView:index:]
RCTPerformMountInstructions(...)
-[RCTMountingManager performTransaction:]
-[RCTMountingManager initiateTransaction:]
__42-[RCTMountingManager scheduleTransaction:]_block_invoke
__RCTExecuteOnMainQueue_block_invoke
_dispatch_call_block_and_release
_dispatch_main_queue_drain
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

Expected result

The list should update selected/disabled visual state without crashing the native app process.

What we tried

The crash stopped after changing the implementation to:

  • No ControlField.
  • No HeroUI Native Checkbox.
  • Plain React Native Pressable rows.
  • No dynamic native disabled prop on the rows.
  • Disabled behavior handled with a JS guard:
onPress={() => {
  if (isDisabled) return;
  toggleItem(item.id);
}}
  • Accessibility state still exposes disabled/checked:
accessibilityRole="checkbox"
accessibilityState={{
  checked: isSelected,
  disabled: isDisabled,
}}
  • Custom checkbox indicator made only with View styles, no Svg.
  • collapsable={false} added to some critical row/container views.

We later reintroduced our scroll shadow wrapper and the crash still did not reproduce, so the main suspect seems to be the combination of:

  • ControlField
  • Checkbox
  • rapid isSelected changes
  • dynamic disabled state in multiple rows
  • React Native Fabric mounting/unmounting

Additional notes

The crash was intermittent but reproducible by aggressively toggling the items. It happened multiple times with the same native stack.

It would be useful to know if ControlField/Checkbox internally mounts/unmounts native children or animated/SVG children when isSelected or isDisabled changes. The native stack suggests React Native Fabric is trying to unmount a child component view at an index that no longer matches the expected native hierarchy.

Please let us know if this is a known issue, if there is a recommended pattern for selectable lists with dynamic disabled rows, or if you want a smaller reproduction project.

Library version

1.0.3

Environment info

Manual environment info from package.json/system:

heroui-native: 1.0.3
expo: 55.0.26
expo-dev-client: 55.0.35
react: 19.2.0
react-native: 0.83.6
react-native-reanimated: 4.2.1
react-native-svg: 15.15.3
react-native-worklets: 0.7.4
react-native-screens: 4.23.0
react-native-gesture-handler: 2.30.0

node: 24.16.0
pnpm: 11.1.2
macOS: 26.5 (25F71)
Xcode: 26.5 (17F42)

Device/runtime:
Platform: iOS physical device
iOS version from crash report: iPhone OS 26.4.2
Build type: Expo dev client / TestFlight-installed dev build

Steps to reproduce

  1. Render a scrollable list of multiple selectable rows using HeroUI Native ControlField.

  2. Each row should contain ControlField.Indicator with a HeroUI Native Checkbox.

  3. Keep selected row IDs in React state.

  4. Dynamically disable unselected rows when a selection limit is reached.

  5. Example behavior:

    • User can select up to N items.
    • Once N items are selected, the remaining unselected rows become disabled.
    • If the user unselects one selected item, the previously disabled rows become enabled again.
  6. Rapidly toggle rows on a physical iOS device:

    • Select item A.
    • Select item B.
    • Unselect item A.
    • Select item C.
    • Repeat quickly.
  7. The app can intermittently abort natively with no useful JS error in Metro.

Observed native crash stack:

Exception Type: EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6

Last Exception Backtrace:
__exceptionPreprocess
objc_exception_throw
-[NSAssertionHandler handleFailureInFunction:file:lineNumber:description:]
-[RCTViewComponentView unmountChildComponentView:index:]
RCTPerformMountInstructions(...)
-[RCTMountingManager performTransaction:]
-[RCTMountingManager initiateTransaction:]
__42-[RCTMountingManager scheduleTransaction:]_block_invoke
__RCTExecuteOnMainQueue_block_invoke

Minimal pseudo-code:

const [selectedIds, setSelectedIds] = useState<string[]>([]);
const limit = 2;

return (
  <ScrollView>
    {items.map((item) => {
      const isSelected = selectedIds.includes(item.id);
      const isDisabled = !isSelected && selectedIds.length >= limit;

      return (
        <ControlField
          key={item.id}
          isSelected={isSelected}
          isDisabled={isDisabled}
          onSelectedChange={(nextSelected) => {
            setSelectedIds((current) => {
              if (nextSelected) return [...current, item.id];
              return current.filter((id) => id !== item.id);
            });
          }}
        >
          <ControlField.Indicator>
            <Checkbox />
          </ControlField.Indicator>

          <View>
            <Text>{item.label}</Text>
          </View>
        </ControlField>
      );
    })}
  </ScrollView>
);

What stopped the crash in our app:

  • Replaced ControlField with plain React Native Pressable.
  • Replaced HeroUI Native Checkbox with a simple View-based checkbox indicator.
  • Avoided changing the native disabled prop dynamically.
  • Kept disabled behavior as a JS guard inside onPress.
  • Kept accessibilityState.checked and accessibilityState.disabled.
  • Added collapsable={false} to some critical row/container views.

After this change, the crash was no longer reproducible even when rapidly toggling the rows.

Reproducible example repository

No public reproducible repository available yet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions