How to implement scroll snapping
Hi, has anyone implemented scroll snapping?
My initial thought was just to put a debounced callback on the container scroll handler, finding the closest virtualItem in the measurements cache and scrolling there with
scrollToOffset
. I was experiencing some janky behavior which I suspect is because scrollToOffset
itself also triggers the scroll handler. So it can sometime queue an unwanted scroll after it snaps to the closest virtual item.4 Replies
fascinating-indigo•3y ago
Maybe something like this
variable-limeOP•3y ago
Thanks. Doesn't this have the same feedback loop problem? When
scrollToIndex
is called, the component will re-render, the layout effect will fire again and so on.fascinating-indigo•3y ago
Ooo true, one option is to wrap the virtualizer with ref, to remove it from deps list. The Latest Ref Pattern, Kent has great blog post about it https://epicreact.dev/the-latest-ref-pattern-in-react/
then you can write and effect that will be triggered after scrolling
The Latest Ref Pattern in React
The Latest Ref Pattern in React
How to improve your custom hook APIs with a simple pattern
variable-limeOP•3y ago
Yeah, I've tried that just now but still get the same janky behaviour. I have a couple of other ideas to try but for now I think I have to move onto other things.
@piecyk For the record, your first snippet does work. No need for the latest ref pattern from what I can tell.
I think the jank I was getting was caused by the
scrollToOffset
calls competing with the user scroll events when the user fired a few in succession with a brief pause between (e.g. repeatedly scrolling a mousewheel).
The trick for me was to debounce the scrollToOffset
call. It seems obvious, and was one of the first things I tried but it wasn't helping at first, for a couple of reasons.
Debouncing an event handler is such a bread-and-butter thing that I just wasn't thinking through the details enough. I am leaving the below info here because it might help someone, someday.
I tried debouncing the event handler itself first, but then the event handler was calling scrollToOffset
(after a short timeout). Of course, that fires another scroll event, so the event handler was causing itself to fire more than once. This results in very janky unexpected scrolls.
Then I tried debouncing an effect instead. I just wasn't thinking this one through. Clearing the debounce timeout in the effect alone is no good, and honestly I should've known better! That way it only gets cleared when the effect runs, (i.e. after the component renders). So sometimes when scrolling a mousewheel repeatedly, a scrollToOffset
timeout would be set, but the component would not re-render before the timeout delay elapsed, and the timeout callback would fire. So, you'd get scrollToOffset
calls interspersed between your mousewheel scrolls.
What worked for me was to set the timeouts in a layout effect, but clear them in the element scroll handler. I suspect there's a tidier way to do this, but I think I need to look at a different problem for a while and come back to it before I see it.
So, code looks something like: