Skip to content

Commit

Permalink
docs: add docs for useVirtualList
Browse files Browse the repository at this point in the history
  • Loading branch information
vikiboss committed Aug 26, 2024
1 parent 429cc3c commit 3896c17
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 41 deletions.
142 changes: 135 additions & 7 deletions src/use-virtual-list/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,158 @@ category: ProUtilities

# useVirtualList

import { HooksType } from '@/components'
import { HooksType, Since } from '@/components'

<HooksType {...frontmatter} />

A React Hook that helps to render large lists more efficiently by only rendering the items that are visible to the user.
<Since version="v1.6.0" />

## 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'

<App />

## 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 (
<div ref={containerRef} style={{ height: 300, overflow: 'auto' }}>
<div ref={wrapperRef}>
{list.map((item) => (
<div key={item.index} style={{ height: 48 }}>
{item.data}
</div>
))}
</div>
</div>
)
```

## Source \{#source}

import { Source } from '@/components'

<Source />

## 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<ElementTarget<HTMLElement>>
/**
* 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<ElementTarget<HTMLElement>>
/**
* 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<D> 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<D> 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<D> = UseVerticalVirtualListOptions<D> | UseHorizontalVirtualListOptions<D>
```
### 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<D> {
/**
* The original data item of the project.
*/
data: D
/**
* The original array index of the project.
*/
index: number
}

export type UseVirtualListReturns<D> = 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<D>[],
/**
* A collection of methods for operating the virtual list.
*/
UseVirtualListReturnsActions,
]
```
66 changes: 38 additions & 28 deletions src/use-virtual-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<D> {
Expand Down Expand Up @@ -138,7 +140,7 @@ export function useVirtualList<D = any>(list: D[], options: UseVirtualListOption

const totalSize = useCreation(() => getOffsetSize(list.length), [list])

function calculateRange() {
function calculateVirtualOffset() {
const containerEl = containerRef.current
const wrapperEl = wrapperRef.current

Expand All @@ -160,25 +162,32 @@ export function useVirtualList<D = any>(list: D[], options: UseVirtualListOption
}))

setWrapperStyle({
[isVerticalLayout ? 'width' : 'height']: '100%',
// [isVerticalLayout ? 'width' : 'height']: '100%',
[isVerticalLayout ? 'height' : 'width']: `${totalSize - offsetSize}px`,
[isVerticalLayout ? 'marginTop' : 'marginLeft']: `${offsetSize}px`,
})

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(() => {
Expand All @@ -188,27 +197,28 @@ export function useVirtualList<D = any>(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
}
Loading

0 comments on commit 3896c17

Please sign in to comment.