Skip to content

Sortable.Grid resize on data change bypasses Yoga: sibling below it keeps a stale hit area (Android/MIUI); any ancestor layout fix causes a visible glitch on MIUI regardless of duration #611

Description

@MaaxiC

Summary

On some Android devices (confirmed on a Xiaomi 11T / MIUI; not reproducible on a Samsung Galaxy Z Flip 7), adding or removing an item from Sortable.Grid's data array (not via drag — via a normal state update) causes:

  1. A visible render glitch for a frame or two around the resize (items appear cut/overlapping), and
  2. Any sibling rendered after the Sortable.Grid in the same flex column keeps a stale touch/hit area — it visually looks fine, but stops responding to taps — until something forces a full native re-layout (e.g. unmounting and remounting the screen).

Both symptoms trace back to the same place: SortableContainer's own container resize doesn't go through a normal Yoga-triggering commit.

Environment

  • react-native-sortables: 1.10.0
  • react-native-reanimated: 4.5.1
  • react-native-worklets: 0.10.1
  • react-native: 0.86.2 (New Architecture / Fabric, always-on at this RN version)
  • Expo SDK 57
  • Repro device: Xiaomi 11T (MIUI). Not reproducible on a Samsung Galaxy Z Flip 7 with the same build.

Our setup

<Sortable.Grid
  columns={1}
  customHandle
  hapticsEnabled
  data={entries} // heterogeneous: mixes two item "kinds" in one sequence
  keyExtractor={(entry) => entry.id}
  renderItem={...}
  rowGap={spacing.sm}
  scrollableRef={scrollableRef}
  sortEnabled={canEdit}
  onDragEnd={({ data }) => ...}
/>

entries is derived from two live DB queries (useLiveQuery) merged and sorted — a normal place/checklist gets added or removed via a separate "Add place" button below the grid, not by dragging.

Root cause (as far as we can tell from the source)

In SortableContainer.tsx:

const outerContainerStyle = useAnimatedStyle(() => {
  ...
  return {
    height: maybeAnimate(
      controlledContainerDimensions.height ? containerHeight.value : null,
      animateWorklet // only true when dimensionsAnimationType === 'worklet'
    ),
    ...
  };
});

<Animated.View
  layout={animateLayout ? LinearTransition : undefined} // only when dimensionsAnimationType === 'layout'
  style={[outerContainerStyle, ...]}
>

With dimensionsAnimationType left at its default ('none'), maybeAnimate returns the raw containerHeight.value and it's applied straight through useAnimatedStyle on the UI thread. That updates the container's own painted box correctly, but (as far as we can tell) never goes through the JS-thread Yoga diff that layout-driven LinearTransition normally participates in — so anything positioned after the grid in a parent flex column never gets repositioned/re-measured for hit-testing when the grid's content-driven height changes. The paint looks right; the touch area doesn't move.

What we tried

  1. Ancestor layout={LinearTransition} wrapping the whole Sortable.Grid region (not inside SortableContainer — one level up, in our own tree): fixes the stale-touch-area problem reliably, at every duration we tried, including LinearTransition.duration(0). But on the Xiaomi 11T, having any animated layout transition there (regardless of duration) reintroduces the visible render glitch. Duration doesn't seem to be the variable — the mere presence of an active Reanimated layout-animation commit at that ancestor is what MIUI's compositor seems to glitch on.
  2. Removing that ancestor's layout prop entirely: fixes the visible glitch on MIUI, but reintroduces the stale-touch-area bug (confirming it's the same underlying issue, not two independent bugs).
  3. dimensionsAnimationType="layout" on Sortable.Grid itself (letting the library apply its own layout={LinearTransition} on SortableContainer's wrapper, per the source above): did not fix the stale-touch-area problem. Our guess is that because the height write on that exact node comes from useAnimatedStyle (UI thread) rather than a normal style/props diff, Reanimated's layout-animation system never detects a "layout changed" event to animate in the first place, so the layout prop is effectively a no-op there.
  4. Remounting Sortable.Grid itself via a key tied to data.length: also did not fix the stale-touch-area problem, ruling out "a fresh mount would just measure correctly" — the same UI-thread-only height write applies to a freshly mounted instance too.

The only thing that ever fixed the stale-touch-area problem was a layout transition living outside SortableContainer, in code we control — which is exactly what breaks on MIUI. We don't have a fix that satisfies both at once; we ended up keeping the ancestor LinearTransition (functional correctness) and accepting the cosmetic MIUI glitch as the lesser problem for now.

Reproduction

We don't have an isolated minimal repro package (this surfaced inside a larger app), but the shape is straightforward to reproduce with a Sortable.Grid of variable-height items where an item is added/removed by mutating data from outside the grid (not by dragging), with something interactive rendered as a sibling after the grid in the same flex container. Happy to help narrow this down further if useful, or to test a patch on the reporting device (Xiaomi 11T) if one is available.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions