-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #75 from umijs/feat/useVirtualList
feat: useVirtualList
- Loading branch information
Showing
6 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { renderHook, act, RenderHookResult } from '@testing-library/react-hooks'; | ||
import { DependencyList } from 'react'; | ||
import useVirtualList, { OptionType } from '../index'; | ||
|
||
/* 暂时关闭 act 警告 见:https://github.com/testing-library/react-testing-library/issues/281#issuecomment-480349256 */ | ||
const originalError = console.error; | ||
beforeAll(() => { | ||
console.error = (...args: any) => { | ||
if (/Warning.*not wrapped in act/.test(args[0])) { | ||
return; | ||
} | ||
originalError.call(console, ...args); | ||
}; | ||
}); | ||
|
||
afterAll(() => { | ||
console.error = originalError; | ||
}); | ||
|
||
describe('useVirtualList', () => { | ||
it('should be defined', () => { | ||
expect(useVirtualList).toBeDefined(); | ||
}); | ||
|
||
describe('virtual list render', () => { | ||
let mockRef = { scrollTop: 0, clientHeight: 300 }; | ||
let hook: RenderHookResult< | ||
{ list: unknown[]; options: OptionType }, | ||
{ | ||
list: unknown[]; | ||
scrollTo: (index: number) => void; | ||
containerProps: { | ||
ref: (ref: any) => void; | ||
}; | ||
wrapperProps: { | ||
style: { | ||
paddingTop: number; | ||
height: number; | ||
}; | ||
}; | ||
} | ||
>; | ||
|
||
const setup = (list: any[] = [], options: {}) => { | ||
hook = renderHook(() => | ||
useVirtualList(list as unknown[], { itemHeight: 30, ...options } as OptionType), | ||
); | ||
hook.result.current.containerProps.ref(mockRef); | ||
}; | ||
|
||
afterEach(() => { | ||
hook.unmount(); | ||
mockRef = { scrollTop: 0, clientHeight: 300 }; | ||
}); | ||
|
||
it('test return list size', () => { | ||
setup(Array.from(Array(99999).keys()), {}); | ||
|
||
act(() => { | ||
hook.result.current.scrollTo(80); | ||
}); | ||
|
||
// 10 items plus 5 overscan * 2 | ||
expect(hook.result.current.list.length).toBe(20); | ||
expect(mockRef.scrollTop).toBe(80 * 30); | ||
}); | ||
|
||
it('test with fixed height', () => { | ||
setup(Array.from(Array(99999).keys()), { overscan: 0 }); | ||
|
||
act(() => { | ||
hook.result.current.scrollTo(20); | ||
}); | ||
|
||
expect(hook.result.current.list.length).toBe(10); | ||
expect(mockRef.scrollTop).toBe(20 * 30); | ||
}); | ||
|
||
it('test with dynamic height', () => { | ||
setup(Array.from(Array(99999).keys()), { | ||
overscan: 0, | ||
itemHeight: (i: number) => (i % 2 === 0 ? 30 : 60), | ||
}); | ||
|
||
act(() => { | ||
hook.result.current.scrollTo(20); | ||
}); | ||
|
||
// average height for easy calculation | ||
const averageHeight = (30 + 60) / 2; | ||
|
||
expect(hook.result.current.list.length).toBe(Math.floor(300 / averageHeight)); | ||
expect(mockRef.scrollTop).toBe(10 * 30 + 10 * 60); | ||
expect((hook.result.current.list[0] as { data: number }).data).toBe(20); | ||
expect((hook.result.current.list[0] as { index: number }).index).toBe(20); | ||
expect((hook.result.current.list[5] as { data: number }).data).toBe(25); | ||
expect((hook.result.current.list[5] as { index: number }).index).toBe(25); | ||
|
||
expect(hook.result.current.wrapperProps.style.paddingTop).toBe(20 * averageHeight); | ||
expect(hook.result.current.wrapperProps.style.height).toBe(99998 * averageHeight + 30); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
--- | ||
name: useVirtualList | ||
route: /useVirtualList | ||
edit: false | ||
sidebar: true | ||
--- | ||
|
||
import { Playground } from 'docz'; | ||
import { InputNumber, Button } from 'antd'; | ||
import useVirtualList from './index'; | ||
|
||
# useVirtualList | ||
|
||
提供虚拟化列表能力的 Hook,用于解决展示海量数据渲染时首屏渲染缓慢和滚动卡顿问题。 | ||
|
||
## 代码演示 | ||
|
||
### 直接使用 | ||
|
||
<Playground> | ||
{ | ||
()=>{ | ||
function Demo(){ | ||
const { list, containerProps, wrapperProps } = useVirtualList(Array.from(Array(99999).keys()), { overscan: 30, itemHeight: 30 }) | ||
return ( | ||
<> | ||
<div {...containerProps} style={{ height: '300px', overflow: 'auto' }}> | ||
<div {...wrapperProps}> | ||
{ | ||
list.map((ele, index)=>{ | ||
return ( | ||
<div | ||
style={{ | ||
height: 22, | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
border: '1px solid #e8e8e8', | ||
marginBottom: 8, | ||
}} | ||
key={ele.index} | ||
> | ||
Row: {ele.data} | ||
</div> | ||
) | ||
}) | ||
} | ||
</div> | ||
</div> | ||
</> | ||
) | ||
} | ||
return <Demo /> | ||
} | ||
} | ||
</Playground> | ||
|
||
### 高级配置 | ||
|
||
<Playground> | ||
{ | ||
()=>{ | ||
function Demo(){ | ||
const [ value, onChange ] = React.useState(''); | ||
const { | ||
list, | ||
containerProps, | ||
wrapperProps, | ||
scrollTo, | ||
} = useVirtualList(Array.from(Array(99999).keys()), { | ||
itemHeight: (i)=> i % 2 === 0 ? 42 + 8 : 84 + 8, | ||
overscan: 10, | ||
}) | ||
return ( | ||
<div> | ||
<div style={{ textAlign: 'right', marginBottom: 16 }}> | ||
<InputNumber min={0} max={99999} placeholder='行数' value={value} onChange={onChange} /> | ||
<Button style={{marginLeft: 8}} onClick={()=>{ scrollTo(Number(value)) }}>滚动到此行</Button> | ||
</div> | ||
<div {...containerProps} style={{ height: '300px', overflow: 'auto' }}> | ||
<div {...wrapperProps}> | ||
{ | ||
list.map((ele, index)=>{ | ||
return ( | ||
<div | ||
style={{ | ||
height: ele.index % 2 === 0 ? 42 : 84, | ||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
border: '1px solid #e8e8e8', | ||
marginBottom: 8, | ||
}} | ||
key={ele.index} | ||
> | ||
Row: {ele.data} size: {ele.index % 2 === 0 ? 'small' : 'large'} | ||
</div> | ||
) | ||
}) | ||
} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
return <Demo /> | ||
} | ||
} | ||
</Playground> | ||
|
||
## API | ||
|
||
``` | ||
const { | ||
list, | ||
containerProps, | ||
wrapperProps, | ||
scrollTo, | ||
} = useVirtualList(originalList, Options); | ||
Options: { | ||
itemHeight: number | ((index: number) => number), | ||
overscan: number, | ||
} | ||
``` | ||
|
||
### 结果 | ||
|
||
| 参数 | 说明 | 类型 | | ||
|----------|------------------------------------------|------------| | ||
| list | 当前需要展示的列表内容 | {data: T, index: number}[] | | ||
| containerProps | 滚动容器的 props | {} | | ||
| wrapperProps | children 外层包裹器 props | {} | | ||
| scrollTo | 快速滚动到指定 index | (index: number) => void | | ||
|
||
### 参数 | ||
|
||
| 参数 | 说明 | 类型 | 默认值 | | ||
|---------|----------------------------------------------|------------------------|--------| | ||
| originalList | 包含大量数据的列表 | T[] | [] | | ||
| options | 可选配置项,见 Options | - | - | | ||
|
||
|
||
### Options | ||
|
||
| 参数 | 说明 | 类型 | 默认值 | | ||
|------|--------------|--------|--------| | ||
| itemHeight | 行高度,静态高度可以直接写入像素值,动态高度可传入函数 | number \| ((index: number) => number) | - | | ||
| overscan | 视区上、下额外展示的 dom 节点数量 | number | 10 | |
Oops, something went wrong.
8966771
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to following URLs: