Skip to content

Commit cba0b56

Browse files
author
Vincent Comby
committed
Refactor to fix performance issues with scroll containers updating each others.
1 parent 1f504db commit cba0b56

File tree

2 files changed

+54
-29
lines changed

2 files changed

+54
-29
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.js

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,68 @@
11
import { useEffect } from 'react'
22

33
/**
4-
* For improve the performance, we are using requestAnimationFrame to throttle
5-
* the number of scrolling events we process to be only 1 per frame.
4+
* Keep in sync the horizontal scroll position of multiple containers. If the user
5+
* scrolls one container to position X, the scroll position of the other containers is
6+
* automatically updated to X.
7+
*
8+
* TODO: The solution could probably be improved by throttling the scroll events (
9+
* using window.requestAninamtionFrame) and using a stack instead of setTimeout
10+
* to know if the scroll events are triggered programmatically or by the user.
611
* @param {Array<Object>} scrollContainerRefList - List of React Refs
712
*/
813
export default function useSyncScroll(scrollContainerRefList) {
14+
915
useEffect(() => {
10-
// Map the list of React ref to the actual list of node.
16+
// Map the list of React refs to the actual list of node.
1117
const containerList = scrollContainerRefList.map(ref => ref.current)
12-
let requestAnimId
18+
let timeoutId
1319

14-
function onScroll(e) {
15-
const scrolledContainer = e.target
20+
// Updating the scrollLeft property manually will trigger a scroll event which
21+
// we want to ignore since it doesn't come from the user and we risk to have the
22+
// container programmatically updated trying to also update the scroll position of the originally
23+
// scrolled container.
24+
// We use this activeContainer state to keep track of which container originally scrolled.
25+
let activeContainer
26+
27+
function syncContainersScroll(scrolledContainer) {
28+
const containersToUpdate = containerList.filter(container => container !== scrolledContainer)
1629

17-
containerList.forEach((container) => {
18-
// Update all containers except the one currently
19-
// scrolling.
20-
if (container && container !== scrolledContainer) {
30+
// Update scroll position of all other containers
31+
containersToUpdate.forEach((container) => {
32+
if (container) {
2133
container.scrollLeft = scrolledContainer.scrollLeft
2234
}
2335
})
24-
// Stop listening to scroll events until next frame
25-
removeScrollListeners()
26-
window.requestAnimationFrame(() => {
27-
// Start listening again to scroll events on the
28-
// next frame.
29-
addScrollListeners()
30-
})
3136
}
3237

33-
function removeScrollListeners() {
34-
if (requestAnimId) {
35-
window.cancelAnimationFrame(requestAnimId)
38+
function onScroll(e) {
39+
const currentlyScrolledContainer = e.target
40+
if (!activeContainer) {
41+
activeContainer = currentlyScrolledContainer
3642
}
43+
44+
// Ignore scroll events in other containers since they
45+
// are very likely to be artificially triggered.
46+
if (currentlyScrolledContainer === activeContainer) {
47+
syncContainersScroll(activeContainer)
48+
49+
// Delay the removal of the active container.
50+
if (timeoutId) {
51+
clearTimeout(timeoutId)
52+
}
53+
54+
// After 200ms without scroll events on the active container
55+
// we reset the state to no containers active.
56+
// We use this window of 200ms without any scroll events to make
57+
// sure that all manually triggered scroll events have already been
58+
// sent before listening to scroll events in all containers again.
59+
timeoutId = setTimeout(() => {
60+
activeContainer = undefined
61+
}, 200)
62+
}
63+
}
64+
65+
function removeScrollListeners() {
3766
containerList.forEach((scrollContainer) => {
3867
if (scrollContainer) {
3968
scrollContainer.removeEventListener('scroll', onScroll)
@@ -42,14 +71,10 @@ export default function useSyncScroll(scrollContainerRefList) {
4271
}
4372

4473
function addScrollListeners() {
45-
requestAnimId = window.requestAnimationFrame(() => {
46-
containerList.forEach((scrollContainer) => {
47-
if (scrollContainer) {
48-
scrollContainer.addEventListener('scroll', onScroll)
49-
}
50-
})
51-
52-
requestAnimId = null
74+
containerList.forEach((scrollContainer) => {
75+
if (scrollContainer) {
76+
scrollContainer.addEventListener('scroll', onScroll)
77+
}
5378
})
5479
}
5580

0 commit comments

Comments
 (0)