Skip to content

Commit

Permalink
Merge pull request #75 from umijs/feat/useVirtualList
Browse files Browse the repository at this point in the history
feat: useVirtualList
  • Loading branch information
brickspert committed Oct 12, 2019
2 parents bc26f28 + de761e1 commit 8966771
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 0 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
rules: {
...fabric.default.rules,
"no-restricted-syntax": "off",
"no-plusplus": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-object-literal-type-assertion": "off",
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-drag-listview": "^0.1.6",
"resize-observer-polyfill": "^1.5.1",
"typescript": "^3.3.3",
"umi-request": "^1.0.8"
},
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import useSearch from './useSearch';
import useControlledValue from './useControlledValue';
import useDynamicList from './useDynamicList';
import useEventEmitter from './useEventEmitter';
import useVirtualList from './useVirtualList';
import { configResponsive, useResponsive } from './useResponsive';
import useSize from './useSize';
import useLocalStorageState from './useLocalStorageState';
Expand All @@ -18,6 +19,7 @@ export {
useSearch,
useControlledValue,
useDynamicList,
useVirtualList,
useResponsive,
useEventEmitter,
useLocalStorageState,
Expand Down
103 changes: 103 additions & 0 deletions src/useVirtualList/__tests__/index.test.ts
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);
});
});
});
149 changes: 149 additions & 0 deletions src/useVirtualList/index.mdx
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 |
Loading

1 comment on commit 8966771

@vercel
Copy link

@vercel vercel bot commented on 8966771 Oct 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.