Skip to content

Instantly share code, notes, and snippets.

@superandrew213
Last active October 3, 2019 12:14
Show Gist options
  • Save superandrew213/22aca60ff7740eadc481b5d9a4eebdca to your computer and use it in GitHub Desktop.
Save superandrew213/22aca60ff7740eadc481b5d9a4eebdca to your computer and use it in GitHub Desktop.
react-window FixedSizeList & FixedSizeGrid position tracker - Can be used for onTopReached, onEndReached, onItemsVisibilityChange
// @flow
import { Component } from 'react'
export type PositionsType = {
visibleColumnStopIndex: number,
visibleRowStartIndex: number,
visibleRowStopIndex: number,
visibleStartIndex?: number,
visibleStopIndex?: number,
}
export type ChangedVisibilityType = {
itemsChangedToHidden: Array<ChangedVisibilityItemType>,
itemsChangedToVisible: Array<ChangedVisibilityItemType>,
}
export type ChangedVisibilityItemType = {
index: number,
item: any,
}
export type Props = {
items: Array<any>,
children: (props: Object) => React$Node,
onTopReached?: () => void,
onEndReached?: () => void,
onItemsVisibilityChange?: (data: ChangedVisibilityType) => void,
}
class PositionTracker extends Component<Props> {
_lastPositions: PositionsType
_lastPositions = null
render() {
return this.props.children({
onItemsRendered: this._onItemsRendered,
})
}
_onItemsRendered = (positionsData: PositionsType): void => {
const { items, onTopReached, onEndReached, onItemsVisibilityChange } = this.props
const positions = this._getPositions(positionsData)
const { visibleRowStartIndex, visibleRowStopIndex } = positions
const scrollDirection = this._getScrollDirection(positions)
const columnCount = this._getColumnCount(positions)
const lastRowIndex = Math.ceil(items.length / columnCount) - 1
// Reached top
if (
onTopReached &&
visibleRowStartIndex === 0 &&
scrollDirection === 'BACKWARD' &&
this._didVisibleRowStartIndexChange(positions)
) {
onTopReached()
}
// Reached bottom
if (
onEndReached &&
visibleRowStopIndex === lastRowIndex &&
scrollDirection === 'FORWARD' &&
this._didVisibleRowStopIndexChange(positions)
) {
onEndReached()
}
// Items that changed visibility
if (onItemsVisibilityChange) {
const itemsThatChangedVisibility = this._getItemsThatChangedVisibility(positions)
if (itemsThatChangedVisibility) {
onItemsVisibilityChange(itemsThatChangedVisibility)
}
}
// Store last positions
this._lastPositions = positions
}
// Support FixedSizeList & FixedSizeGrid
_getPositions = (positionsData: PositionsType): PositionsType => ({
visibleColumnStopIndex: positionsData.visibleColumnStopIndex,
visibleRowStartIndex: positionsData.visibleStartIndex != null ? (
positionsData.visibleStartIndex
) : (
positionsData.visibleRowStartIndex
),
visibleRowStopIndex: positionsData.visibleStopIndex != null ? (
positionsData.visibleStopIndex
) : (
positionsData.visibleRowStopIndex
),
})
_getColumnCount = (positions: PositionsType): number => {
return positions.visibleColumnStopIndex ? positions.visibleColumnStopIndex + 1 : 1
}
_getScrollDirection = (positions: PositionsType) => {
const {
visibleRowStartIndex,
visibleRowStopIndex,
} = positions
if (this._lastPositions == null) {
return
}
if (
this._lastPositions.visibleRowStartIndex > visibleRowStartIndex ||
this._lastPositions.visibleRowStopIndex > visibleRowStopIndex
) {
return 'BACKWARD'
}
if (
this._lastPositions.visibleRowStartIndex < visibleRowStartIndex ||
this._lastPositions.visibleRowStopIndex < visibleRowStopIndex
) {
return 'FORWARD'
}
}
_getItemsThatChangedVisibility = (positions: PositionsType): ChangedVisibilityType | void => {
const { items } = this.props
const {
visibleRowStartIndex,
visibleRowStopIndex,
} = positions
const scrollDirection = this._getScrollDirection(positions)
// Ensure scroll has started
if (!scrollDirection) {
return
}
const columnCount = this._getColumnCount(positions)
const itemsChangedToHidden = []
const itemsChangedToVisible = []
const visibleRowStartIndexChanged = this._didVisibleRowStartIndexChange(positions)
const visibleRowStopIndexChanged = this._didVisibleRowStopIndexChange(positions)
// Hidden
const hiddenItemsStartIndex = scrollDirection === 'FORWARD' ? (
visibleRowStartIndexChanged ? (visibleRowStartIndex - 1) * columnCount : null
) : (
visibleRowStopIndexChanged ? (visibleRowStopIndex + 1) * columnCount : null
)
// Visible
const visibleItemsStartIndex = scrollDirection === 'FORWARD' ? (
visibleRowStopIndexChanged ? visibleRowStopIndex * columnCount : null
) : (
visibleRowStartIndexChanged ? visibleRowStartIndex * columnCount : null
)
for (let i = 0; i < columnCount; i += 1) {
if (hiddenItemsStartIndex != null) {
itemsChangedToHidden.push({
index: hiddenItemsStartIndex + i,
item: items[hiddenItemsStartIndex + i],
})
}
if (visibleItemsStartIndex != null) {
itemsChangedToVisible.push({
index: visibleItemsStartIndex + i,
item: items[visibleItemsStartIndex + i],
})
}
}
return {
itemsChangedToHidden,
itemsChangedToVisible,
}
}
_didVisibleRowStartIndexChange = (positions: PositionsType): boolean => {
return (
this._lastPositions != null &&
this._lastPositions.visibleRowStartIndex !== positions.visibleRowStartIndex
)
}
_didVisibleRowStopIndexChange = (positions: PositionsType): boolean => {
return (
this._lastPositions != null &&
this._lastPositions.visibleRowStopIndex !== positions.visibleRowStopIndex
)
}
}
export default PositionTracker
@superandrew213
Copy link
Author

superandrew213 commented Aug 26, 2018

Using it for grids, but should work for lists too.

    <RWPositionTracker
      items={items}
      onTopReached={this._onTopReached}
      onEndReached={this._onEndReached}
      onItemsVisibilityChange={this._onItemsVisibilityChange}
    >
      {({ onItemsRendered }) => (
        <FixedSizeGrid
          ...
          onItemsRendered={onItemsRendered}
        >
          ...
        </FixedSizeGrid >
      )}
    </RWPositionTracker>

and

    _onItemsVisibilityChange = ({ itemsChangedToHidden, itemsChangedToVisible }) => {
       itemsChangedToVisible.forEach(({ index, item }) => console.log({ index, item }))
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment