Skip to content

Instantly share code, notes, and snippets.

@ExcitedSpider
Created August 2, 2021 06:35
Show Gist options
  • Save ExcitedSpider/edbb2781d8c09ab16cf34eb2826a5bd0 to your computer and use it in GitHub Desktop.
Save ExcitedSpider/edbb2781d8c09ab16cf34eb2826a5bd0 to your computer and use it in GitHub Desktop.
A react hook to make a virtual scroll container
import React, { useState, useEffect, useRef } from 'react';
import clamp from 'lodash/clamp';
export type VScrollController = { scrollTo: (top: number) => void };
const DEFAULT_DURATION = '200ms';
const DEFAULT_TIMEING_FUNCTION = 'ease-in-out';
/**
* virtual scroll 的实现
*
* 把 return 出来的 domAttributes 对象直接放到虚拟滚动列表容器上即可
*
* @example
* const {domAttributes} = useVirtualScroll()
* return <div {...domAttributes}></div>
*/
export const useVirtualScroll = (
options: {
/**
* 如果列表长度会变化,传入列表长度依赖的值数组
* 本 hook 会监听其变化重新计算虚拟滚动相关参数
*/
listDependency?: any[];
/**
* 控制器 ref
*/
controllerRef?: React.RefObject<VScrollController>;
/**
* 在 controller scrollTo 时采用的动画配置
*/
transitionDuration?: string;
transitionTimingFunction?: string;
} = {},
) => {
const { listDependency = [] } = options;
const [vScrollY, setVScrollY] = useState(0);
const [maxScrollY, setMaxScrollY] = useState(0);
const scrollContentRef = useRef<HTMLDivElement>(null);
const [inTransition, setInTransition] = useState(false);
// 初始化计算最大滚动高度
useEffect(() => {
if (scrollContentRef.current) {
setMaxScrollY(scrollContentRef.current?.scrollHeight - scrollContentRef.current?.clientHeight);
scrollContentRef.current.style.transitionDuration = options.transitionDuration || DEFAULT_DURATION;
scrollContentRef.current.style.transitionTimingFunction =
options.transitionTimingFunction || DEFAULT_TIMEING_FUNCTION;
scrollContentRef.current.style.transitionProperty = 'none';
}
}, [scrollContentRef.current, ...listDependency]);
// 构造 controller scrollTo 功能和动画
useEffect(() => {
const onTransitionEnd = (e: TransitionEvent) => {
setInTransition(false);
if (e.propertyName === 'transform' && scrollContentRef.current) {
scrollContentRef.current.style.transitionProperty = 'none';
}
};
if (scrollContentRef.current) {
scrollContentRef.current.addEventListener('wheel', (e) => e.preventDefault());
scrollContentRef.current.addEventListener('transitionend', onTransitionEnd);
}
if (options.controllerRef) {
// eslint-disable-next-line no-param-reassign
(options.controllerRef as React.MutableRefObject<VScrollController>).current = {
scrollTo: (top: number) => {
if (scrollContentRef.current) {
scrollContentRef.current.style.transitionProperty = 'transform';
setInTransition(true);
}
setVScrollY(0 - Math.min(top, maxScrollY));
},
};
}
return () => {
if (scrollContentRef.current) {
scrollContentRef.current.removeEventListener('transitionend', onTransitionEnd);
}
};
}, [options.controllerRef?.current, scrollContentRef.current, vScrollY]);
const handleVScroll = (e: React.WheelEvent) => {
const { deltaY } = e;
if (inTransition && scrollContentRef.current) {
scrollContentRef.current.style.transitionProperty = 'none';
setInTransition(false);
}
// 滚动位置最小到顶,最大到底
setVScrollY(clamp(vScrollY - deltaY, 0 - maxScrollY, 0));
};
return {
domAttributes: {
ref: scrollContentRef,
style: { transform: `translateY(${vScrollY}px)`, height: '100%' },
onWheelCapture: handleVScroll,
},
maxScrollY,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment