ryer.io

Five Rounds With an Auto-Scroll That Wouldn't Trigger

TL;DR

  • Dragging an item near the edge of the list should auto-scroll. It didn’t.
  • The nestable scroll container was measuring against its parent’s reference, not its own.
  • Drag-and-drop overrides the responder, so the parent scroll view never sees the threshold event.
  • Wrapping only the draggable components — not the whole screen — fixed the height calculation.
  • A React Native upgrade removed behaviour that used to be automatic; it now needs wiring by hand.

Days on one bug. Writing it as the sequence of wrong theories, because the wrong ones are where the time went.

The symptom

A react-native-draggable-flatlist in a nested scrolling situation. Dragging an item toward the edge of the screen should scroll the list. It didn’t hit the thresholds, and once I’d scrolled down I couldn’t drag back up without releasing first.

The component had grown over time and become brittle, which made everything harder — comparing the rock queue screen against the daily plan screen showed the same component behaving differently in each, so the difference had to be contextual rather than in the list itself.

Theory one: a styling conflict

I suspected the unified list’s styling rather than the nesting. Swapping the screen container for a standard view partly fixed a visibility problem and did nothing for auto-scroll.

I stripped it right back — commenting out extraneous components and effects, dropping animations, moving from a section setup to nestableScrollContainer, keeping the view style constant. Dragging improved somewhat and the core problem stayed. A boolean or threshold wasn’t resetting on reaching the drag limit.

Useful negative result: scrollable matters and imperative handles behave oddly across tabs, but neither explains it.

Theory two: the container measures the wrong thing

This one was right, and it took a while to see.

The nestable scroll container was using its parent’s reference to calculate scroll height rather than its own. So every threshold calculation was against the wrong dimensions, which is exactly why dragging worked in one direction depending on what I’d dragged previously — the numbers were wrong in a way that depended on prior state.

Refactoring so the unified list acts as its own scroll container was the fix in principle. In practice it produced this.props.onscroll is not a function, thrown constantly. Progress that looks like regression.

The wrapping was too greedy

Related and more concrete. I’d wrapped the entire screen content in the scroll container, which is why the heights were wrong — static content was being included in measurements meant to describe the draggable region.

Wrapping only the draggable components, leaving static content outside, resolved a good part of it. Adding a View with 50% height between the scroll container and the flat list gave drag and scroll a bounded region to work within, and the behaviour improved again.

The rule that emerged: nothing with flex: 1 should wrap the draggable components, and the nestable scroll container’s placement in the hierarchy is load-bearing rather than incidental.

The responder system

The last piece explains why none of the styling work could ever have fixed this.

Drag-and-drop overrides the responder. Once the drag gesture claims it, the parent scroll view never receives the threshold events that would trigger auto-scroll. The parent isn’t ignoring the drag — it isn’t being told about it.

So the responder has to be configured manually to let the parent respond. I tried simultaneous handlers: one managing the drag, another on the parent view watching for threshold breaches and initiating the scroll. Wrapping the draggable in the correct context isolated calculations to adjacent list items, which disabled auto-scrolling through some interaction I still don’t fully understand.

Current approach is scroll refs — attach a reference to the parent scroll view and invoke scrolling programmatically when the threshold is crossed. If the framework won’t propagate the event, do it explicitly.

What actually changed

A React Native upgrade altered this behaviour. What used to be built-in now needs manual intervention, which is why code that worked stopped working without anyone touching it.

That reframes the whole exercise. I spent days looking for my mistake, and the mistake was assuming the framework still did something it had quietly stopped doing. Worth checking the upgrade notes before assuming the bug is yours.