Skip to content

Compute totalSize and itemSizeAndPositionData at the start when using a fixed or array as itemSize #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/types
npm-debug.log*
.DS_Store
yarn.lock
6 changes: 5 additions & 1 deletion demo/src/demo.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ header img {
margin-right: 15px;
}

.Root {
padding: 20px;
}

.VirtualList {
margin: 20px;
margin: 40px 0;
background: #FFF;
border-radius: 2px;
box-shadow:
Expand Down
116 changes: 104 additions & 12 deletions demo/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,121 @@ import * as ReactDOM from 'react-dom';
import VirtualList, {ItemStyle} from '../../src';
import './demo.css';

class Demo extends React.Component {
const range = N => Array.from({length: N}, (_, k) => k + 1);

class FixedItemSize extends React.Component {
renderItem = ({style, index}: {style: ItemStyle; index: number}) => {
return (
<div
className="Row"
style={{...style, backgroundColor: index % 2 ? '#eee' : 'white'}}
key={index}
>
Row #{index + 1}
</div>
);
};

render() {
return (
<VirtualList
width="auto"
height={400}
itemCount={10000}
renderItem={this.renderItem}
itemSize={50}
className="VirtualList"
/>
);
}
}

class ArrayItemSize extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
items: range(10000).map(() => {
return Math.max(Math.ceil(Math.random() * 200), 50);
}),
};
}

renderItem = ({style, index}: {style: ItemStyle; index: number}) => {
return (
<div className="Row" style={style} key={index}>
Row #{index}
<div
className="Row"
style={{...style, backgroundColor: index % 2 ? '#eee' : 'white'}}
key={index}
>
Row #{index + 1}. Height: {this.state.items[index]}
</div>
);
};

render() {
return (
<VirtualList
width="auto"
height={400}
itemCount={this.state.items.length}
renderItem={this.renderItem}
itemSize={this.state.items}
className="VirtualList"
/>
);
}
}

class FunctionItemSize extends React.Component<any, any> {
constructor(props: any) {
super(props);
this.state = {
items: range(10000).map(() => {
return Math.max(Math.ceil(Math.random() * 100), 50);
}),
};
}

renderItem = ({style, index}: {style: ItemStyle; index: number}) => {
return (
<div
className="Row"
style={{...style, backgroundColor: index % 2 ? '#eee' : 'white'}}
key={index}
>
Row #{index + 1}. Height: {this.state.items[index]}
</div>
);
};

render() {
return (
<VirtualList
width="auto"
height={400}
itemCount={this.state.items.length}
renderItem={this.renderItem}
estimatedItemSize={75}
itemSize={index => this.state.items[index]}
className="VirtualList"
/>
);
}
}

class Demos extends React.Component {
render() {
return (
<div className="Root">
<VirtualList
width="auto"
height={400}
itemCount={1000}
renderItem={this.renderItem}
itemSize={50}
className="VirtualList"
/>
<h2>Fixed itemSize</h2>
<FixedItemSize />
<h2>Array itemSize (mixed heights)</h2>
<ArrayItemSize />
<h2>Function itemSize (just-in-time calculation)</h2>
<FunctionItemSize />
</div>
);
}
}

ReactDOM.render(<Demo />, document.querySelector('#app'));
ReactDOM.render(<Demos />, document.querySelector('#app'));
100 changes: 84 additions & 16 deletions src/SizeAndPositionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ interface SizeAndPositionData {
}

export interface Options {
itemSize: ItemSize;
itemCount: number;
itemSizeGetter: ItemSizeGetter;
estimatedItemSize: number;
}

export default class SizeAndPositionManager {
private itemSizeGetter: ItemSizeGetter;
private itemCount: number;
private estimatedItemSize: number;
private itemSize: ItemSize;
private lastMeasuredIndex: number;
private estimatedItemSize: number;
private totalSize?: number;
private itemSizeAndPositionData: SizeAndPositionData;

constructor({itemCount, itemSizeGetter, estimatedItemSize}: Options) {
this.itemSizeGetter = itemSizeGetter;
constructor({itemSize, itemCount, estimatedItemSize}: Options) {
this.itemSize = itemSize;
this.itemCount = itemCount;
this.estimatedItemSize = estimatedItemSize;

Expand All @@ -36,13 +37,19 @@ export default class SizeAndPositionManager {

// Measurements for items up to this index can be trusted; items afterward should be estimated.
this.lastMeasuredIndex = -1;

this.checkForMismatchItemSizeAndItemCount();

if (!this.justInTime) {
this.computeTotalSizeAndPositionData();
}
}

get justInTime() {
return typeof this.itemSize === 'function';
}

updateConfig({
itemCount,
itemSizeGetter,
estimatedItemSize,
}: Partial<Options>) {
updateConfig({itemSize, itemCount, estimatedItemSize}: Partial<Options>) {
if (itemCount != null) {
this.itemCount = itemCount;
}
Expand All @@ -51,9 +58,55 @@ export default class SizeAndPositionManager {
this.estimatedItemSize = estimatedItemSize;
}

if (itemSizeGetter != null) {
this.itemSizeGetter = itemSizeGetter;
if (itemSize != null) {
this.itemSize = itemSize;
}

this.checkForMismatchItemSizeAndItemCount();

if (this.justInTime && this.totalSize != null) {
this.totalSize = undefined;
} else {
this.computeTotalSizeAndPositionData();
}
}

checkForMismatchItemSizeAndItemCount() {
if (Array.isArray(this.itemSize) && this.itemSize.length < this.itemCount) {
throw Error(
`When itemSize is an array, itemSize.length can't be smaller than itemCount`,
);
}
}

getSize(index: number) {
const {itemSize} = this;

if (typeof itemSize === 'function') {
return itemSize(index);
}

return Array.isArray(itemSize) ? itemSize[index] : itemSize;
}

/**
* Compute the totalSize and itemSizeAndPositionData at the start,
* only when itemSize is a number or an array.
*/
computeTotalSizeAndPositionData() {
let totalSize = 0;
for (let i = 0; i < this.itemCount; i++) {
const size = this.getSize(i);
const offset = totalSize;
totalSize += size;

this.itemSizeAndPositionData[i] = {
offset,
size,
};
}

this.totalSize = totalSize;
}

getLastMeasuredIndex() {
Expand All @@ -62,7 +115,6 @@ export default class SizeAndPositionManager {

/**
* This method returns the size and position for the item at the specified index.
* It just-in-time calculates (or used cached values) for items leading up to the index.
*/
getSizeAndPositionForIndex(index: number) {
if (index < 0 || index >= this.itemCount) {
Expand All @@ -71,13 +123,23 @@ export default class SizeAndPositionManager {
);
}

return this.justInTime
? this.getJustInTimeSizeAndPositionForIndex(index)
: this.itemSizeAndPositionData[index];
}

/**
* This is used when itemSize is a function.
* just-in-time calculates (or used cached values) for items leading up to the index.
*/
getJustInTimeSizeAndPositionForIndex(index: number) {
if (index > this.lastMeasuredIndex) {
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();
let offset =
lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size;

for (let i = this.lastMeasuredIndex + 1; i <= index; i++) {
const size = this.itemSizeGetter(i);
const size = this.getSize(i);

if (size == null || isNaN(size)) {
throw Error(`Invalid size returned for index ${i} of value ${size}`);
Expand Down Expand Up @@ -105,10 +167,16 @@ export default class SizeAndPositionManager {

/**
* Total size of all items being measured.
* This value will be completedly estimated initially.
* As items as measured the estimate will be updated.
*/
getTotalSize(): number {
// Return the pre computed totalSize when itemSize is number or array.
if (this.totalSize) return this.totalSize;

/**
* When itemSize is a function,
* This value will be completedly estimated initially.
* As items as measured the estimate will be updated.
*/
const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem();

return (
Expand Down
16 changes: 2 additions & 14 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,9 @@ export default class VirtualList extends React.PureComponent<Props, State> {
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
};

itemSizeGetter = (itemSize: Props['itemSize']) => {
return index => this.getSize(index, itemSize);
};

sizeAndPositionManager = new SizeAndPositionManager({
itemCount: this.props.itemCount,
itemSizeGetter: this.itemSizeGetter(this.props.itemSize),
itemSize: this.props.itemSize,
estimatedItemSize: this.getEstimatedItemSize(),
});

Expand Down Expand Up @@ -189,7 +185,7 @@ export default class VirtualList extends React.PureComponent<Props, State> {

if (nextProps.itemSize !== itemSize) {
this.sizeAndPositionManager.updateConfig({
itemSizeGetter: this.itemSizeGetter(nextProps.itemSize),
itemSize: nextProps.itemSize,
});
}

Expand Down Expand Up @@ -388,14 +384,6 @@ export default class VirtualList extends React.PureComponent<Props, State> {
);
}

private getSize(index: number, itemSize) {
if (typeof itemSize === 'function') {
return itemSize(index);
}

return Array.isArray(itemSize) ? itemSize[index] : itemSize;
}

private getStyle(index: number, sticky: boolean) {
const style = this.styleCache[index];

Expand Down
Loading