Skip to content

Instantly share code, notes, and snippets.

@xiel
Created January 30, 2020 11:12
Show Gist options
  • Save xiel/3b6a2974269623cbcc5b384ee6434a3d to your computer and use it in GitHub Desktop.
Save xiel/3b6a2974269623cbcc5b384ee6434a3d to your computer and use it in GitHub Desktop.
useScrollDirection react hook
import { useEffect, useRef, useState } from 'react'
import { getDocumentScrollPos } from '../helpers/getDocumentScrollPos'
import isPassiveSupported from '../helpers/isPassiveSupported'
type AxisName = 'x' | 'y'
type ScrollDir = '' | 'positive' | 'negative'
interface ScrollDirState {
dir: ScrollDir
hitThreshold: boolean
isAtMin: boolean
}
interface ScrollDirXY {
x: ScrollDirState
y: ScrollDirState
}
const axes: AxisName[] = ['x', 'y']
const initialScrollDir: ScrollDirXY = {
x: {
dir: '',
hitThreshold: false,
isAtMin: false,
},
y: {
dir: '',
hitThreshold: false,
isAtMin: false,
},
}
interface Options {
averageFromLatestCount?: number
threshold?: number
}
export function useScrollDirection({ averageFromLatestCount = 1, threshold = 200 }: Options = {}) {
const latestScrollPos = useRef(getDocumentScrollPos())
const latestScrollDeltas = useRef({
x: [0],
y: [0],
})
const deltaSumSinceDirChange = useRef({
x: 0,
y: 0,
})
const [scrollDirection, setScrollDirection] = useState(initialScrollDir)
const setAxisSD = (axis: AxisName, state: Partial<ScrollDirState>) =>
setScrollDirection(current => ({
...current,
[axis]: {
...current[axis],
...state,
},
}))
useEffect(() => {
const onScroll = () => {
const currScrollPos = getDocumentScrollPos()
axes.forEach(axis => {
const delta = currScrollPos[axis] - latestScrollPos.current[axis]
const axisLatestScrollDeltas = latestScrollDeltas.current[axis]
axisLatestScrollDeltas.unshift(delta)
if (axisLatestScrollDeltas.length > averageFromLatestCount) {
axisLatestScrollDeltas.length = averageFromLatestCount
}
const average =
axisLatestScrollDeltas.reduce((a, b) => a + b, 0) / axisLatestScrollDeltas.length
const axisDir = average === 0 ? '' : average > 0 ? 'positive' : 'negative'
deltaSumSinceDirChange.current[axis] += delta
// detect dir change and update exported state
if (scrollDirection[axis].dir !== axisDir) {
// reset delta sum
deltaSumSinceDirChange.current[axis] = 0
// update the new scroll dir and reset hit threshold
setAxisSD(axis, {
dir: axisDir,
hitThreshold: false,
})
}
// check if threshold was hit, if so update state
if (
!scrollDirection[axis].hitThreshold &&
Math.abs(deltaSumSinceDirChange.current[axis]) >= threshold
) {
setAxisSD(axis, {
hitThreshold: true,
})
}
// update the atMin state
const isAtMin = currScrollPos[axis] <= 0
if (scrollDirection[axis].isAtMin !== isAtMin) {
setAxisSD(axis, {
isAtMin,
})
}
// save current scroll pos for next delta calculation
latestScrollPos.current[axis] = currScrollPos[axis]
})
}
window.addEventListener('scroll', onScroll, isPassiveSupported() ? { passive: true } : false)
return () => window.removeEventListener('scroll', onScroll)
}, [averageFromLatestCount, scrollDirection, threshold])
return scrollDirection
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment