Socket
Socket
Sign inDemoInstall

react-infinite-grid-scroller

Package Overview
Dependencies
Maintainers
1
Versions
83
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-infinite-grid-scroller

infinite vertical or horizontal scroll using css grid layout


Version published
Weekly downloads
7
decreased by-93.64%
Maintainers
1
Weekly downloads
 
Created
Source

react-infinite-grid-scroller (RIGS)

Heavy-duty vertical or horizontal infinite scroller

npm licence

Key Features

  • vertical or horizontal scrolling
  • designed for both "heavy" and "light" cell content (React components)
  • supports both uniform and variable cell lengths (for both vertical and horizontal)
  • single or multiple rows or columns
  • dynamically variable virtual list size; prepend or append
  • limited sparse in-memory cache, to preserve content state, with an API
  • repositioning mode when rapidly scrolling (such as by using the scroll thumb)
  • dynamic pivot (horizontal/vertical back and forth) while maintaining position in list
  • automatic reconfiguration with viewport resize
  • dynamic recalibration with async content refresh
  • supports nested RIGS scrollers

Demo Site

See the demo site.

Key Technologies

RIGS uses these key technologies:

Therefore RIGS is best suited for modern browsers.

Architecture

Architecture

Notes: The Cradle is kept in view of the Viewport, such that the axis is always near the top or left of the Viewport (depending on vertical or horizontal orientation). There are two CSS grids in the Cradle, one on each side of the axis. As CellFrames are added to or removed from the grids, the grid on the top or left expands toward or contracts away from the top or left of the Scrollblock (depending on orientation), and the grid on the bottom or right expands toward or contracts away from the bottom or right of the Scrollblock.

CellFrames display individual user components. CellFrames are created and destroyed on a rolling basis as the Cradle re-configures and moves around the Scrollblock to stay in view, but user components are maintained in the internal cache until they go out of scope. New CellFrames fetch user components from the internal cache (portals in the React virtual DOM) or from the host through the user-supplied getItem function, as needed.

Not shown are two triggerlines (0 width or height divs, depending on orientation) which straddle the top or left edge of the Viewport. Whenever one of these triggerlines crosses the Viewport edge (through scrolling), an IntersectionObserver sends an interrupt to the Cradle to update its content and configuration. Triggerlines are located in the last CellFrame of the head grid, unless the scroller is at the very top of its list, in which case the triggerlines are located in the first CellFrame of the tail grid.

Usage

This is the minimum configuration.

import Scroller from 'react-infinite-grid-scroller'

// ...

const lowindex = -50, highindex = 50 // random range values

<div style = { containerstyle }>
  <Scroller 
      cellHeight = { cellHeight }
      cellWidth = { cellWidth }
      startingListRange = { [lowindex, highindex] } // this constitutes the virtual list
      getItem = { getItem } // a function called by RIGS to obtain a specified user component by index number
  />
</div>

The scroller's highest level component, the Viewport, is a div with position:absolute, and inset:0, so the host container should be styled accordingly.

Note that scroller-generated elements show a data-type attribute in browser inspectors (eg. 'viewport').

User components loaded to CellFrames are placed in a data-type = 'contentenvelope' div. In 'uniform' layout this has position:absolute and inset:0. In 'variable' layout it has width:100% and max-height = cellHeight for 'vertical' orientation, and height:100% and max-width = cellWidth for 'horizontal' orientation. In any case it has overflow:hidden.

See the "Content Types" section of the demodata.tsx module of the demo site for more code examples.

Animated GIF

This is a random screenshare, showing nested scrollers in a resized browser (33% normal).

animation

Compatible browsers

RIGS works on Chrome, Microsoft Edge, Firefox and Safari.

chrome edge firefox safari

Scroller properties

propertyvaluenotes
[REQUIRED]
cellHeight:integernumber of pixels for cell heightrequired. Applied to height for 'uniform' layout, 'vertical' orientation. Applied to max-height for 'variable' layout, 'vertical' orientation. Approximate, used for fr (fractional allocation) for 'horizontal' orientation
cellWidth:integernumber of pixels for cell widthrequired. Applied to width for 'uniform' layout, 'horizontal' orientation. Applied to max-width for 'variable' layout, 'horizontal' orientation. Approximate, used for fr (fractional allocation) for 'vertical' orientation
startingListSize:integerthe starting number of items in the virtual listrequired. Can be modified at runtime. Constitutes a 0-based virtual array (Internally creates a starting range of [0,startingListSize - 1]. Ignored in the presence of startingListRange array
startingListRange:[lowindex, highindex] | []two part array , or empty array []lowindex must be <= highindex; both can be positive or negative integers. [] (empty array) creates an empty virtual list
getItem(index:integer,itemID:integer):
React.FC | Promise | undefined | null
host-provided function. session itemID (integer) is for tracking and matching. Arguments provided by systemrequired. Must return a React component or promise of a component (React.isValidElement), or undefined = unavailable, or null = end-of-list
[SCROLLER OPTIONS]
orientation:string'vertical' (default) or 'horizontal'direction of scroll
layout:string'uniform' (default) or 'variable'specifies handling of the height or width of cells, depending on orientation. 'uniform' is fixed cellHeight/cellWidth. 'variable' is constrained by cellHeight/cellWidth (maximum) and cellMinHeight/cellMinWidth (minimum)
startingIndex:integerstarting index when the scroller first loadsdefault = 0
padding:integer | []number of pixels padding the Scrollblockdefault = 0; accepts an array of integers as well as a standalone integer. Values match standard CSS order. Standalone integer = padding (in pixels) for all of top, right, bottom, left. 1-item array, same as integer. 2-item array = [t/b, r/l]. 3-item array = [t, r/l, b]. 4-item array = [t, r, b, l]
[MORE CELL OPTIONS]
gap:integer | []number of pixels between cellsthere is no gap at start or end of rows or columns; default = 0; accepts an array of integers as well as a standalone integer. Values match standard CSS order. Standalone integer = gap (in pixels) for both of column-gap (horizontal) and row-gap (vertical). 1-item array, same as integer. 2-item array = [col-gap, row-gap]
cellMinHeight:integerdefault = 25, minimum = 25, maximum = cellHeightused for 'variable' layout with 'vertical' orientation. Applied to min-height
cellMinWidth:integerdefault = 25, minimum = 25, maximum = cellWidthused for 'variable' layout with 'horizontal' orientation. Applied to min-width
[SYSTEM SETTINGS]
runwaySize:integernumber of rows in the Cradle just out of view at head and tail of listdefault = 1. minimum = 1. Gives time to assemble cellFrames before display
cache:string'cradle' (default), 'keepload', 'preload''cradle' matches the cache to the contents of the Cradle. 'keepload' keeps user components in the cache as loaded, up to cacheMax (and always Cradle user components). 'preload' loads user components up to cacheMax, then adjusts cache such that Cradle user components are always in the cache
cacheMax:integerat minimum (maintained by system) the number of user components in the Cradleallows optimization of cache size for memory limits and performance
useScrollTracker:booleandefault = trueallows suppression of system feedback on position within list while in reposition mode, if the host wants to provide alternative feedback based on data from callbacks
placeholder:React.FCa lightweight React component for cellFrames to load while waiting for the intended cellFrame componentsoptional (replaces default placeholder). parameters are index, listsize, message, error. Arguments set by system
usePlaceholder:booleandefault = trueallows suppression of use of default or custom placeholder. Placeholders show messages to the user while user components are fetched, and report errors
cacheAPI:nullrequested by user components by being set to null by user; instantiated with a class instance by systemExperimental. If present, parent scroller instantiates the property with its cacheAPI instance, which causes any child scroller given the property to share the parent scroller cache. This currently has no operational effect
[OBJECT PROPERTIES]
styles:Objectcollection of styles for scroller componentsoptional. These should be "passive" styles like backgroundColor. See below for details
placeholderMessages:Objectmessages presented by the placeholderoptional, to replace default messages. See below for details
callbacks:Objectcollection of functions for feedback, and interactions with scroller componentsoptional. See below for details
technical:Objectcollection of values used to control system behaviouruse with caution. optional. See below for details
scrollerProperties:nullrequested by user components by being set to null by user; instantiated with an object by systemrequired for nested RIGS; available for all user components. Contains key scroller settings. See below for details

Notes: For explicit cache management capability, a unique session itemID (integer) is assigned to a user component as soon as it enters the cache. The itemID is retired as soon as the user component is removed from the cache. If the same component is re-introduced into the cache, it is assigned a new session-unique itemID.

The itemID for a user component is given to the host with the getItem call to obtain the component, so that the host can track the user component in the cache. If the user component is assigned to a new index number (see the returned function object cache management section below) the host will still be able to track the user component with the itemID.

The host can track removal of a user component and its itemID from the cache through tracking its associated index removal through the deleteListCallback return value, and the return values from cache management functions.

Most of the time the itemID can be ignored.

Also, note that the cache is reloaded with a new getItem function.

styles object

Create a style object for each of the elements you want to modify. The styles are not screened, though the RIGS essential styles pre-empt user styles. Be careful to only include "passive" styles (like color, backgroundColor) so as not to confuse the scroller. Do not add structural items like borders, padding etc.

styles = {
  viewport: {}, 
  scrollblock: {}, 
  cradle: {},
  scrolltracker: {},
  placeholderframe: {},
  placeholderliner: {},
  placeholdererrorframe: {},
  placeholdererrorliner: {},
}

You may want to add overscrollBehavior:'none' to the top level viewport styles to prevent inadvertent reloading of your app in certain browsers when users scroll past the top of a vertical list.

The scrolltracker is the small rectangular component that appears at the top left of the viewport when the list is being rapidly repositioned. The scrolltracker shows the user the current virtual position (index + 1) and total listsize during the repositioning process.

The placeholder styles are applied only to the default placeholder.

placeholderMessages object

Replace any of the default messages used by the placeholder.

const placeholderMessages = {
    loading:'(loading...)',
    retrieving:'(retrieving from cache)',
    null:'end of list', // is returned with itemExceptionCallback
    undefined:'host returned "undefined"', // displayed, and returned with itemExceptionCallback
    invalid:'invalid React element', // displayed, and returned with itemExceptionCallback
}

callbacks object

Callbacks are host-defined closure functions which the Cradle calls to provide data back to the host. Cradle returns data by setting the arguments of the callbacks. Include only the callbacks in the callbacks object that you want the Cradle to use. The following are recognized by the Cradle:

callbacks: {

     // called at setup...
     functionsCallback, // (functions) - get an object that has api functions
     
     // index tracking, called when triggered...
     referenceIndexCallback, // (index, location, cradleState) - change of index adjacent to the axis
     repositioningIndexCallback, // (index) - current virtual index number during rapid repositioning
     preloadIndexCallback, // (index) - current index being preloaded
     itemExceptionCallback, // (index, itemID, returnvalue, location, error) - details about failed getItem calls

     // operations tracking, called when triggered
     changeListSizeCallback, // (newlistsize) - triggered when the listsize changes for any reason
     deleteListCallback, // (reason, deleteList) - data about which items have been deleted from the cache
     repositioningFlagCallback, // (flag) - notification of start (true) or end (false) of rapid repositioning
     
}

An example of a callback closure (functionsCallback):

const scrollerFunctionsRef = useRef(null)

const functionsCallback = (functions) => {

    scrollerFunctionsRef.current = functions // assign the returned functions object to a local Ref

}

//...

scrollerFunctionsRef.current.scrollToIndex(targetIndex)

Details about the callbacks:

callback function(parameters:datatypes)notes
[GET FUNCTIONS]
functionsCallback(functions: object)the object returned contains Cradle functions that the host can call directly. This is the API. functionsCallback is called once at startup. See below for details
[TRACK INDEXES]
referenceIndexCallback(index: integer, location: string, cradleState: string)location can be 'setCradleContent', 'updateCradleContent'. Keeps the host up to date on the index number adjacent to the Cradle axis, and the state change that triggered the update
repositioningIndexCallback(index: integer)the current index during repositioning. Useful for feedback to user when host sets useScrollTracker property to false
preloadIndexCallback(index: integer)during a preload operation, this stream gives the index number being preloaded
itemExceptionCallback(index: integer, itemID: integer, returnvalue: any, location: string, error: Error)triggered whenever getItem does not return a valid React component
[TRACK OPERATIONS]
changeListsizeCallback(newlistsize: integer)notification of a change of list size. Could be from getItem returning null indicating end-of-list, or an API call that results in change of list size
deleteListCallback(reason: string, deleteList: array)gives an array of indexes that have been deleted from the cache, and text of the reason
repositioningFlagCallback(flag: boolean)called with true when repositioning starts, and false when repositioning ends. Useful for feedback to user when host sets useScrollTracker property to false

returned API functions object

Details about the functions returned in an object by functionsCallback:

function(parameters: datatypes):return value: datatypenotes
[OPERATIONS]
scrollToIndex(index:integer):voidplaces the requested index item at the top visible row or left visible column of the scroller, depending on orientation
scrollToPixel(pixel:integer[,behavior:string = 'smooth']):voidscrolls the scroller to the provided pixel, along the current orientation. behavior = 'smooth' | 'instant' | 'auto'; default = 'smooth'. pixel must be >=0
scrollByPixel(pixel:integer[,behavior:string = 'smooth']):voidscrolls the scroller up or down by the number of provided pixels, along the current orientation. behavior = 'smooth' | 'instant' | 'auto'; default = 'smooth'. pixel can be positive (scroll down) or negative (scroll up)
setListsize(index:integer):voidchanges the list size, by adjusting the list range high index. Favour use of setListRange instead
setListRange(array [lowindex, highindex] | []):voidlowindex must be <= highindex; lowindex and highindex can be positive or negative integers. [] (empty array) creates an empty virtual list
prependIndexCount(count:integer):voidthe number of indexes to expand the start of the virtual list
appendIndexCount(count:integer):voidthe number of indexes to expand the end of the virtual list
[SNAPSHOTS]
getCacheIndexMap():Mapsnapshot of cache index (=key) to itemID (=value) map
getCacheItemMap():Mapsnapshot of cache itemID (=key) to object (=value) map. Object = {index, component} where component = user component
getCradleIndexMap(): Mapsnapshot of Cradle index (=key) to itemID (=value) map
getPropertiesSnapshot():objectcopy of scrollerPropertiesRef.current from scrollerProperties object. See below.
[CACHE MANAGEMENT]
insertIndex(index:integer, rangehighindex: integer | null):array[changeList:array, replaceList:array, removeList:array]can insert a range of indexes. Displaced indexes, and higher indexes, are renumbered; virtual list lowindex remains the same. Changes the list size by increasing virtual list highindex; synchronizes the Cradle
removeIndex(index:integer, rangehighindex:integer | null):array[changeList:array, replaceList:array, removeList:array]a range of indexes can be removed. Higher indexes are renumbered; virtual list lowindex remains the same. Changes the list size by decreasing virtual list highindex; synchronizes to the Cradle
moveIndex(toindex:integer, fromindex:integer, fromhighrange: integer | null):array[processedIndexList:array]a range of indexes can be moved. Displaced and higher indexes are renumbered. Changes the list size; synchronizes to the Cradle
remapIndexes(changeMap:Map):array[
modifiedIndexList: array,
processedIndexList: array,
deletedIndexList: array,
indexesOfReplacedItemsList: array,
deletedOrphanedItemIDList: array,
deletedOrphanedIndexList: array,
errorEntriesMap: Map,
changeMap: Map]
(return changeMap is the same as input parameter). changeMap is index (=key) to itemID (=value) map. indexes or itemIDs not in the cache are ignored. indexes with values set to null are deleted. indexes with values set to undefined have their component items replaced. itemIDs are assigned to the new indexes; synchronizes to the Cradle. List size is adjusted as necessary
reload():voidclears the cache and reloads the Cradle at its current position in the virtual list
clearCache():voidclears the cache and the Cradle (leaving nothing to display)

Notes: cache management functions are provided to support drag-n-drop, sorting, and filtering operations.

Cache management functions operate on indexes and itemIDs in the cache, and generally ignore indexes and itemIDs that are not in the cache. They synchronize Cradle cell content as appropriate.

This is a sparse in-memory cache, and indexes in the cache are not guaranteed to be contiguous.

technical object

These properties would rarely be changed.

property:datatype = defaultnotes
showAxis:boolean = falseaxis can be made visible for debug
triggerlineOffset:integer = 10distance from cell head or tail for content shifts above/below axis
VIEWPORT_RESIZE_TIMEOUT:integer = 250milliseconds before the Viewport resizing state is cleared
ONAFTERSCROLL_TIMEOUT:integer = 100milliseconds after last scroll event before onAfter scroll event is fired
IDLECALLBACK_TIMEOUT:integer = 175milliseconds timeout for requestIdleCallback
VARIABLE_MEASUREMENTS_TIMEOUT:integer = 250milliseconds to allow setCradleContent changes to render before being measured for 'variable' layout
CACHE_PARTITION_SIZE:integer = 30the cache is partitioned for performance reasons
MAX_CACHE_OVER_RUN:number = 1.5max streaming cache size over-run (while scrolling) as ratio to cacheMax

scrollerProperties object

Cell components can get access to dynamically updated parent RIGS properties, by requesting the scrollerProperties object.

The scrollerProperties object is requested by user components by initializing a scrollerProperties component property to null. The property is then recognized by RIGS and set to the scrollerProperties object by the system on loading of the component to a CellFrame.

the scrollerProperties object contains three properties:

{
  cellFramePropertiesRef,
  scrollerPropertiesRef
}

Each of these is a reference object, with values found in propertyRef.current.

The cellFramePropertiesRef.current object is instantiated only when a component is instantiated in the cradle. It contains two properties:

{
  itemID, // session cache itemID
  index // place in virtual list
}

The scrollerPropertiesRef.current object contains the following properties, which are identical to the properties set for the scroller (they are passed through):

orientation, gap, padding, cellHeight, cellWidth, cellMinHeight, cellMinWidth, layout, cache, cacheMax, startingIndex

It also contains scrollerID, the internal session id (integer) of the current scroller, for debug purposes.

Finally, it contains two objects with bundled properties: virtualListProps and cradleContentProps.

virtualListProps is an object with the following properties:

{
   size, // the length (number of virtual cells) of the virtual list
   range, // a two-part array [lowindex,highindex]
   lowindex, // the virtual list low index
   highindex, // the virtual list high index
   baserowblanks, // cell offset count in the first row
   endrowblanks, // blank cells at the end of the last row
   crosscount, // number of cells perpendicular to the orientation
   rowcount, // number of rows in virtual list
   rowshift, // row shift from zero to accommodate lowindex
}

cradleContentProps is an object with the following properties:

{
   cradleRowcount, // number of rows in the cradle (including any blank cells)
   viewportRowcount, // number of full rows that can be shown in the viewport
   runwayRowcount, // calculated current extra leading and trailing cell rows beyond the viewport boundary
   SOL, // true or false, at start of virtual list in the cradle
   EOL, // true or false, at end of virtual list in the cradle
   lowindex, // of cells in the cradle
   highindex, // of cells in the cradle
   axisReferenceIndex, // the first index of the tail grid
   size, // count of cells in the cradle
}

Restoring scroll positions coming out of cache

This is only of concern if your cell components support scrolling.

RIGS loads components into a cache (React portals), and into Cradle cells from there. Moreover RIGS moves components from one side of the (hidden) axis to the other through the cache during scrolling. Plus caching can be extended (by RIGS property settings) beyond the Cradle. So going into and out of cache happens a lot for components. While in cache, the component elements have their scrollTop, scrollLeft, width, and height values set to 0 by browsers. width and height values are restored by browsers when moved back into the visible DOM area, but scroll positions have to be manually restored.

Here is one way of restoring scroll positions. Basically, save scroll positions on an ongoing basis, detect going into cache when width and height values are both zero, and detect coming out of cache when width and height are no longer zero. When coming out of cache, restore the saved scroll positions.

This code is Typescript, in a function component.

    // ------------------------[ handle scroll position recovery ]---------------------

    // define required data repo
    const scrollerElementRef = useRef<any>(null),
        scrollPositionsRef = useRef({scrollTop:0, scrollLeft:0}),
        wasCachedRef = useRef(false)

    // define the scroll event handler - save scroll positions
    const scrollerEventHandler = (event:React.UIEvent<HTMLElement>) => {

        const scrollerElement = event.currentTarget

        // save scroll positions if the scroller element is not cached
        if (!(!scrollerElement.offsetHeight && !scrollerElement.offsetWidth)) {

            const scrollPositions = scrollPositionsRef.current

            scrollPositions.scrollTop = scrollerElement.scrollTop
            scrollPositions.scrollLeft = scrollerElement.scrollLeft

        }

    }

    // register the scroll event handler
    useEffect( () => {

        const scrollerElement = scrollerElementRef.current

        scrollerElement.addEventListener('scroll', scrollerEventHandler)

        // unmount
        return () => {
            scrollerElement.removeEventListener('scroll', scrollerEventHandler)
        }

    },[])

    // define the cache sentinel - restore scroll positions
    const cacheSentinel = () => {
        const scrollerElement = scrollerElementRef.current

        if (!scrollerElement) return // first iteration

        const isCached = (!scrollerElement.offsetWidth && !scrollerElement.offsetHeight) // zero values == cached

        if (isCached != wasCachedRef.current) { // there's been a change

            wasCachedRef.current = isCached

            if (!isCached) { // restore scroll positions

                const {scrollTop, scrollLeft} = scrollPositionsRef.current

                scrollerElement.scrollTop = scrollTop
                scrollerElement.scrollLeft = scrollLeft

            }

        }

    }

    // run the cache sentinel on every iteration
    cacheSentinel()

    // register the scroller element through the ref attribute
    return <div ref = {scrollerElementRef} style = {scrollerstyles}>
        {scrollercontent}
    </div>

Licence

MIT © 2020-2023 Henrik Bechmann

Keywords

FAQs

Package last updated on 21 Aug 2023

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc