From 3896c174114aae3ca045960d48ab360ecc486e10 Mon Sep 17 00:00:00 2001 From: Viki Date: Mon, 26 Aug 2024 20:03:29 +0800 Subject: [PATCH] docs: add docs for `useVirtualList` --- src/use-virtual-list/index.mdx | 142 +++++++++++++++++++++++++-- src/use-virtual-list/index.tsx | 66 +++++++------ src/use-virtual-list/index.zh-cn.mdx | 140 ++++++++++++++++++++++++-- 3 files changed, 307 insertions(+), 41 deletions(-) diff --git a/src/use-virtual-list/index.mdx b/src/use-virtual-list/index.mdx index 32a4712c..dfb97011 100644 --- a/src/use-virtual-list/index.mdx +++ b/src/use-virtual-list/index.mdx @@ -4,30 +4,158 @@ category: ProUtilities # useVirtualList -import { HooksType } from '@/components' +import { HooksType, Since } from '@/components' -A React Hook that helps to render large lists more efficiently by only rendering the items that are visible to the user. + -## Demo +A React Hook that helps to render large lists (known as **Virtual List**) more efficiently by rendering only the items that are visible to the user, supporting dynamic heights, horizontal and vertical scrolling, and more. + +## Scenes \{#scenes} + +- Long list performance optimization: By only rendering the elements within the visible area, the scrolling performance of long lists is optimized. +- Dynamic content size handling: Supports dynamically calculating and rendering the items within the visible area based on the content size. + +## Demo \{#demo} import { App } from './demo' -## Usage +## Usage \{#usage} + +```tsx +const containerRef = useRef(null) +const wrapperRef = useRef(null) + +const [list, actions] = useVirtualList(largeList, { + // or use `querySelector` string directly: `#scroll-container` + containerTarget: containerRef, + // or use `querySelector` string directly: `#scroll-wrapper` + wrapperTarget: wrapperRef, + // ensure that the height of each item is 48 (including margin, padding, etc.) + // or a function that returns the height of each item + itemHeight: 48, +}) -See API for more details. +// actions.scrollTo(1000) +// actions.scrollToStart() +// actions.scrollToEnd() -## Source +// use `list[0].data` and `list[0].index` to render the item + +return ( +
+
+ {list.map((item) => ( +
+ {item.data} +
+ ))} +
+
+) +``` + +## Source \{#source} import { Source } from '@/components' -## API +## API \{#api} ```tsx +const [list, actions] = useVirtualList(largeList, options) +``` + +### LargeList \{#large-list} + +An arbitrary valid array containing a large amount of data. + +### Options \{#options} + +```tsx +interface UseVirtualListBaseOptions { + /** + * The container element, of type ElementTarget, typically the scrollable element. + */ + containerTarget: NonNullable> + /** + * The wrapper element, of type ElementTarget, typically the element containing all items. + * + * MarginTop and height (marginLeft and width for horizontal scrolling) will be set to the total height (width) of all items. + */ + wrapperTarget: NonNullable> + /** + * The number of items to additionally render outside the viewport (above and below for vertical scrolling, left and right for horizontal). + * + * @defaultValue 5 + */ + overscan?: number +} + +interface UseVerticalVirtualListOptions extends UseVirtualListBaseOptions { + /** + * The height of each item, or a function returning the height of each item, accepting parameters index and item. + * + * When `itemHeight` is set, the list is in vertical rendering mode, with higher priority than `itemWidth`. + */ + itemHeight: number | ((index: number, item: D) => number) +} + +interface UseHorizontalVirtualListOptions extends UseVirtualListBaseOptions { + /** + * The width of each item, or a function returning the width of each item, accepting parameters index and item. + * + * When `itemWidth` is set and `itemHeight` is not, the list is in horizontal rendering mode. + */ + itemWidth: number | ((index: number, item: D) => number) +} + +export type UseVirtualListOptions = UseVerticalVirtualListOptions | UseHorizontalVirtualListOptions +``` + +### Returns \{#returns} + +```tsx +export interface UseVirtualListReturnsActions { + /** + * Scrolls to the item at the specified index. + * + * @param {Number} index The index of the item to scroll to. + */ + scrollTo: (index: number) => void + /** + * Scrolls to the start of the list, automatically using vertical or horizontal scrolling based on the options. + */ + scrollToStart: () => void + /** + * Scrolls to the end of the list, automatically using vertical or horizontal scrolling based on the options. + */ + scrollToEnd: () => void +} + +export interface UseVirtualListReturnsListItem { + /** + * The original data item of the project. + */ + data: D + /** + * The original array index of the project. + */ + index: number +} +export type UseVirtualListReturns = readonly [ + /** + * The array of items of the virtual list, i.e., the actual items that are being rendered, including the visible area and the additional rendering area. + */ + list: UseVirtualListReturnsListItem[], + /** + * A collection of methods for operating the virtual list. + */ + UseVirtualListReturnsActions, +] ``` diff --git a/src/use-virtual-list/index.tsx b/src/use-virtual-list/index.tsx index 40ff51a7..18058f7b 100644 --- a/src/use-virtual-list/index.tsx +++ b/src/use-virtual-list/index.tsx @@ -2,7 +2,9 @@ import { useRef } from 'react' import { useCreation } from '../use-creation' import { useElementSize } from '../use-element-size' import { useEventListener } from '../use-event-listener' +import { useLatest } from '../use-latest' import { useSafeState } from '../use-safe-state' +import { useStableFn } from '../use-stable-fn' import { useTargetElement } from '../use-target-element' import { useUpdateEffect } from '../use-update-effect' import { isNumber } from '../utils/basic' @@ -54,15 +56,15 @@ export interface UseVirtualListReturnsActions { * * @param {Number} index The index of the item to scroll to. */ - scrollTo: (index: number, options?: ScrollOptions) => void + scrollTo: (index: number) => void /** * Scroll to the start of the list, automatically use vertical or horizontal scrolling based on the options. */ - scrollToStart: (options?: ScrollOptions) => void + scrollToStart: () => void /** * Scroll to the end of the list, automatically use vertical or horizontal scrolling based on the options. */ - scrollToEnd: (options?: ScrollOptions) => void + scrollToEnd: () => void } export interface UseVirtualListReturnsListItem { @@ -138,7 +140,7 @@ export function useVirtualList(list: D[], options: UseVirtualListOption const totalSize = useCreation(() => getOffsetSize(list.length), [list]) - function calculateRange() { + function calculateVirtualOffset() { const containerEl = containerRef.current const wrapperEl = wrapperRef.current @@ -160,7 +162,7 @@ export function useVirtualList(list: D[], options: UseVirtualListOption })) setWrapperStyle({ - [isVerticalLayout ? 'width' : 'height']: '100%', + // [isVerticalLayout ? 'width' : 'height']: '100%', [isVerticalLayout ? 'height' : 'width']: `${totalSize - offsetSize}px`, [isVerticalLayout ? 'marginTop' : 'marginLeft']: `${offsetSize}px`, }) @@ -168,17 +170,24 @@ export function useVirtualList(list: D[], options: UseVirtualListOption setRenderList(renderList) } + const latest = useLatest({ + list, + itemSize, + getOffsetSize, + calculateVirtualOffset, + }) + useEventListener(containerRef, 'scroll', () => { if (isTriggeredInternallyRef.current) { isTriggeredInternallyRef.current = false return } - calculateRange() + calculateVirtualOffset() }) // biome-ignore lint/correctness/useExhaustiveDependencies: The effect should only run when the container size changes. - useUpdateEffect(() => void calculateRange(), [containerSize.height, containerSize.width, list]) + useUpdateEffect(() => void calculateVirtualOffset(), [containerSize.height, containerSize.width, list]) // biome-ignore lint/correctness/useExhaustiveDependencies: The effect should only run when wrapper styles change. useUpdateEffect(() => { @@ -188,27 +197,28 @@ export function useVirtualList(list: D[], options: UseVirtualListOption } }, [wrapperStyle]) - const actions = useCreation(() => ({ - scrollTo(index: number, scrollOptions?: ScrollOptions) { - const containerEl = containerRef.current - if (!containerEl) return - - isTriggeredInternallyRef.current = true - const offsetSize = isNumber(itemSize) ? index * itemSize : getOffsetSize(index) - containerEl[isVerticalLayout ? 'scrollTop' : 'scrollLeft'] = offsetSize - - // Manually trigger the `calculateRange` instantly, use ref to mark the manual trigger, - // because subscription of `useEventListener` will not be triggered in the same frame. - // This is useful to prevent layout jittering when scrolling. - calculateRange() - }, - scrollToStart(scrollOptions?: ScrollToOptions) { - this.scrollTo(0, scrollOptions) - }, - scrollToEnd(scrollOptions?: ScrollToOptions) { - this.scrollTo(list.length - 1, scrollOptions) - }, - })) + const scrollTo = useStableFn((index: number) => { + const containerEl = containerRef.current + if (!containerEl) return + + isTriggeredInternallyRef.current = true + + const offsetSize = isNumber(latest.current.itemSize) + ? index * latest.current.itemSize + : latest.current.getOffsetSize(index) + + containerEl[isVerticalLayout ? 'scrollTop' : 'scrollLeft'] = offsetSize + + // Manually trigger the `calculateVirtualOffset` instantly, use ref to mark the manual trigger, + // because subscription of `useEventListener` will not be triggered in the same frame. + // This is useful to prevent layout jittering when scrolling. + latest.current.calculateVirtualOffset() + }) + + const scrollToStart = useStableFn(() => scrollTo(0)) + const scrollToEnd = useStableFn(() => scrollTo(latest.current.list.length - 1)) + + const actions = useCreation(() => ({ scrollTo, scrollToStart, scrollToEnd })) return [renderList, actions] as const } diff --git a/src/use-virtual-list/index.zh-cn.mdx b/src/use-virtual-list/index.zh-cn.mdx index 32a4712c..856c91ab 100644 --- a/src/use-virtual-list/index.zh-cn.mdx +++ b/src/use-virtual-list/index.zh-cn.mdx @@ -4,23 +4,61 @@ category: ProUtilities # useVirtualList -import { HooksType } from '@/components' +import { HooksType, Since } from '@/components' -A React Hook that helps to render large lists more efficiently by only rendering the items that are visible to the user. + -## Demo +一个通过只渲染用户可见项目来帮助更高效地渲染大型列表的 React Hook,即**虚拟列表**,支持动态高度,横向、纵向滚动等场景。 + +## 场景 + +- 长列表性能优化: 通过只渲染可视区域内的元素,优化长列表的滚动性能 +- 动态内容尺寸处理: 支持基于内容尺寸动态计算并渲染可视区内的项目 + +## Demo 演示 import { App } from './demo' -## Usage +## Usage 用法 + +```tsx +const containerRef = useRef(null) +const wrapperRef = useRef(null) + +const [list, actions] = useVirtualList(largeList, { + // 或者直接使用 `querySelector` 的字符串: `#scroll-container` + containerTarget: containerRef, + // 或者直接使用 `querySelector` 的字符串: `#scroll-wrapper` + wrapperTarget: wrapperRef, + // 确保每个项目的高度为 48(包括 margin、padding 等),或者一个返回每个项目高度的函数 + itemHeight: 48, +}) -See API for more details. +// actions.scrollTo(1000) +// actions.scrollToStart() +// actions.scrollToEnd() -## Source +// 使用 `list[0].data` 和 `list[0].index` 来渲染项目 + +return ( +
+
+ {list.map((item) => ( +
+ {item.data} +
+ ))} +
+
+) +``` + + +## Source 源码 import { Source } from '@/components' @@ -29,5 +67,95 @@ import { Source } from '@/components' ## API ```tsx +const [list, actions] = useVirtualList(largeList, options) +``` + +### LargeList + +一个包含大量数据的任意合法数组。 + +### Options + +```tsx +interface UseVirtualListBaseOptions { + /** + * 容器元素,ElementTarget 类型,通常是可滚动的元素。 + */ + containerTarget: NonNullable> + /** + * Wrapper 元素,ElementTarget 类型,通常是包含所有项目的元素。 + * + * marginTop 和 height(横向滚动时为 marginLeft 和 width)将被设置为所有项目的总高度(宽度)。 + */ + wrapperTarget: NonNullable> + /** + * 在视野外(竖直滚动时为上下,横向为左右)分别额外渲染的项目数量。默认为 5。 + * + * @defaultValue 5 + */ + overscan?: number +} + +interface UseVerticalVirtualListOptions extends UseVirtualListBaseOptions { + /** + * 每个项目的高度,或者返回每个项目高度的函数,接受参数为 index 和 item。 + * + * 当设置了 `itemHeight` 时,列表为纵向渲染模式,优先级高于 `itemWidth`。 + */ + itemHeight: number | ((index: number, item: D) => number) +} + +interface UseHorizontalVirtualListOptions extends UseVirtualListBaseOptions { + /** + * 每个项目的宽度,或者返回每个项目宽度的函数,接受参数为 index 和 item。 + * + * 当设置了 `itemWidth` 且未设置 `itemHeight` 时,列表为横向渲染模式。 + */ + itemWidth: number | ((index: number, item: D) => number) +} + +export type UseVirtualListOptions = UseVerticalVirtualListOptions | UseHorizontalVirtualListOptions +``` + +### Returns + +```tsx +export interface UseVirtualListReturnsActions { + /** + * 滚动到指定索引的项目。 + * + * @param {Number} index 想要滚动到的项目索引。 + */ + scrollTo: (index: number) => void + /** + * 滚动到列表的开始,根据选项自动使用纵向或横向滚动。 + */ + scrollToStart: () => void + /** + * 滚动到列表的结束,根据选项自动使用纵向或横向滚动。 + */ + scrollToEnd: () => void +} + +export interface UseVirtualListReturnsListItem { + /** + * 项目的原始数据项。 + */ + data: D + /** + * 项目的原始数组索引。 + */ + index: number +} +export type UseVirtualListReturns = readonly [ + /** + * 虚拟列表的项目数组,即实际被渲染的项目列表,包含可视区域和额外渲染区域。 + */ + list: UseVirtualListReturnsListItem[], + /** + * 操作虚拟列表的方法集合。 + */ + UseVirtualListReturnsActions, +] ```