Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork40
😎 ♻️ A tiny React hook for rendering large datasets like a breeze.
License
wellyshen/react-cool-virtual
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A tiny React hook for rendering large datasets like a breeze.
- ♻️ Renders millions of items with highly performant way, usingDOM recycling.
- 🎣 Easy to use, based on Reacthook.
- 💅🏼 Apply styles without hassle, justfew setups.
- 🧱 Supportsfixed,variable,dynamic, andreal-time heights/widths.
- 🖥 Supportsresponsive web design (RWD) for better UX.
- 📌 Supportssticky headers for building on-trend lists.
- 🚚 Built-insload more callback for you to deal with infinite scroll +skeleton screens.
- 🖱 Imperativescroll-to methods for offset, items, and alignment.
- 🛹 Out-of-the-boxsmooth scrolling and the effect is DIY-able.
- 💬 It's possible to implementstick to bottom andpre-pending items for chat, feeds, etc.
- ⛳ Provides
isScrollingindicator to you for UI placeholders orperformance optimization. - 🗄️ Supportsserver-side rendering (SSR) for a fastFP + FCP and betterSEO.
- 📜 SupportsTypeScript type definition.
- 🎛 Super flexibleAPI design, built with DX in mind.
- 🦔 Tiny size (~ 3.1kB gzipped). No external dependencies, aside from the
react.
When rendering a large set of data (e.g. list, table, etc.) in React, we all face performance/memory troubles. There'resome great libraries already available but most of them are component-based solutions that provide well-defineded way of using but increase a lot of bundle size. However,a library comes out as a hook-based solution that is flexible andheadless but using and styling it can be verbose (because it's a low-level hook). Furthermore, it lacks many of theuseful features.
React Cool Virtual is atiny React hook that gives you abetter DX andmodern way for virtualizing a large amount of data without struggle 🤯.
To use React Cool Virtual, you must usereact@16.8.0 or greater which includes hooks.
This package is distributed vianpm.
$ yarn add react-cool-virtual# or$ npm install --save react-cool-virtual
⚠️ This package usingResizeObserver API under the hook.Most modern browsers support it natively, you can also addpolyfill for full browser support.
If you're not using a module bundler or package manager. We also provide aUMD build which is available over theunpkg.com CDN. Simply use a<script> tag to add it afterReact CDN links as below:
<scriptcrossoriginsrc="https://unpkg.com/react/umd/react.production.min.js"></script><scriptcrossoriginsrc="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script><!-- react-cool-virtual comes here --><scriptcrossoriginsrc="https://unpkg.com/react-cool-virtual/dist/index.umd.production.min.js"></script>
Once you've added this you will have access to thewindow.ReactCoolVirtual.useVirtual variable.
Here's the basic concept of how it rocks:
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:10000,// Provide the total number for the list itemsitemSize:50,// The size of each item (default = 50)});return(<divref={outerRef}// Attach the `outerRef` to the scroll containerstyle={{width:"300px",height:"500px",overflow:"auto"}}>{/* Attach the `innerRef` to the wrapper of the items */}<divref={innerRef}>{items.map(({ index, size})=>(// You can set the item's height with the `size` property<divkey={index}style={{height:`${size}px`}}> ⭐️{index}</div>))}</div></div>);};
✨ Pretty easy right? React Cool Virtual is more powerful than you think. Let's explore more use cases through the examples!
This example demonstrates how to create a fixed size row. For column or grid, please refer to CodeSandbox.
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<divkey={index}style={{height:`${size}px`}}> ⭐️{index}</div>))}</div></div>);};
This example demonstrates how to create a variable size row. For column or grid, please refer to CodeSandbox.
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,itemSize:(idx)=>(idx%2 ?100 :50),});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<divkey={index}style={{height:`${size}px`}}> ⭐️{index}</div>))}</div></div>);};
This example demonstrates how to create a dynamic (unknown) size row. For column or grid, please refer to CodeSandbox.
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,itemSize:75,// The unmeasured item sizes will refer to this value (default = 50)});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, measureRef})=>(// Use the `measureRef` to measure the item size<divkey={index}ref={measureRef}>{/* Some data... */}</div>))}</div></div>);};
💡 The scrollbar is jumping (or unexpected position)? It's because the total size of the items is gradually corrected along with an item that has been measured. You can tweak the
itemSizeto reduce the phenomenon.
This example demonstrates how to create a real-time resize row (e.g. accordion, collapse, etc.). For column or grid, please refer to CodeSandbox.
import{useState,forwardRef}from"react";importuseVirtualfrom"react-cool-virtual";constAccordionItem=forwardRef(({ children, height, ...rest},ref)=>{const[h,setH]=useState(height);return(<div{...rest}style={{height:`${h}px`}}ref={ref}onClick={()=>setH((prevH)=>(prevH===50 ?100 :50))}>{children}</div>);});constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:50,});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size, measureRef})=>(// Use the `measureRef` to measure the item size<AccordionItemkey={index}height={size}ref={measureRef}> 👋🏻 Click Me</AccordionItem>))}</div></div>);};
This example demonstrates how to create a list with RWD to provide a better UX for the user.
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,// Use the outer's width (2nd parameter) to adjust the item's sizeitemSize:(_,width)=>(width>400 ?50 :100),// The event will be triggered on outer's size changesonResize:(size)=>console.log("Outer's size: ",size),});return(<divstyle={{width:"100%",height:"400px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{/* We can also access the outer's width here */}{items.map(({ index, size, width})=>(<divkey={index}style={{height:`${size}px`}}> ⭐️{index} ({width})</div>))}</div></div>);};
💡 If the item size is specified through the function of
itemSize, please ensure there's no themeasureRef on the item element. Otherwise, the hook will use the measured (cached) size for the item. When working with RWD, we can only use either of the two.
This example demonstrates how to make sticky headers with React Cool Virtual.
importuseVirtualfrom"react-cool-virtual";constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,itemSize:75,stickyIndices:[0,10,20,30,40,50],// The values must be provided in ascending order});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size, isSticky})=>{letstyle={height:`${size}px`};// Use the `isSticky` property to style the sticky item, that's it ✨style=isSticky ?{ ...style,position:"sticky",top:"0"} :style;return(<divkey={someData[index].id}style={style}>{someData[index].content}</div>);})}</div></div>);};
💡 Forbetter performance & accessibility. We encourage you to add
will-change:transformto the positioned elements to render the element in its own layer, improving repaint speed and therefore improving performance and accessibility.
💡 The scrollbar disappears when using Chrome in Mac? If you encounterthis issue, you can add
will-change:transformto the outer element to workaround this problem.
You can imperatively scroll to offset or items as follows:
const{ scrollTo, scrollToItem}=useVirtual();constscrollToOffset=()=>{// Scrolls to 500pxscrollTo(500,()=>{// 🤙🏼 Do whatever you want through the callback});};constscrollToItem=()=>{// Scrolls to the 500th itemscrollToItem(500,()=>{// 🤙🏼 Do whatever you want through the callback});// We can control the alignment of the item with the `align` option// Acceptable values are: "auto" (default) | "start" | "center" | "end"// Using "auto" will scroll the item into the view at the start or end, depending on which is closerscrollToItem({index:10,align:"auto"});};
React Cool Virtual provides the smooth scrolling feature out of the box, all you need to do is turn thesmooth option on.
const{ scrollTo, scrollToItem}=useVirtual();// Smoothly scroll to 500pxconstscrollToOffset=()=>scrollTo({offset:500,smooth:true});// Smoothly scroll to the 500th itemconstscrollToItem=()=>scrollToItem({index:10,smooth:true});
💡 When working withdynamic size, the scroll position will be automatically corrected along with the items are measured. To optimize it, we can provide an estimated item size to theitemSize option.
The default easing effect iseaseInOutSine, and the duration is100ms <= distance * 0.075 <= 500ms. You can easily customize your own effect as follows:
const{ scrollTo}=useVirtual({// For 500 millisecondsscrollDuration:500,// Or whatever duration you want based on the scroll distancescrollDuration:(distance)=>distance*0.05,// Using "easeInOutBack" effect (default = easeInOutSine), see: https://easings.net/#easeInOutSinescrollEasingFunction:(t)=>{constc1=1.70158;constc2=c1*1.525;returnt<0.5 ?(Math.pow(2*t,2)*((c2+1)*2*t-c2))/2 :(Math.pow(2*t-2,2)*((c2+1)*(t*2-2)+c2)+2)/2;},});constscrollToOffset=()=>scrollTo({offset:500,smooth:true});
💡 For more cool easing effects, pleasecheck it out.
It's possible to make a complicated infinite scroll logic simple by just using a hook, no kidding! Let's see how possible 🤔.
Working withSkeleton Screens
import{useState}from"react";importuseVirtualfrom"react-cool-virtual";importaxiosfrom"axios";constTOTAL_COMMENTS=500;constBATCH_COMMENTS=5;constisItemLoadedArr=[];constloadData=async({ loadIndex},setComments)=>{// Set the state of a batch items as `true`// to avoid the callback from being invoked repeatedlyisItemLoadedArr[loadIndex]=true;try{const{data:comments}=awaitaxios(`/comments?postId=${loadIndex+1}`);setComments((prevComments)=>{constnextComments=[...prevComments];comments.forEach((comment)=>{nextComments[comment.id-1]=comment;});returnnextComments;});}catch(err){// If there's an error set the state back to `false`isItemLoadedArr[loadIndex]=false;// Then try againloadData({ loadIndex},setComments);}};constList=()=>{const[comments,setComments]=useState([]);const{ outerRef, innerRef, items}=useVirtual({itemCount:TOTAL_COMMENTS,// Estimated item size (with padding)itemSize:122,// The number of items that you want to load/or pre-load, it will trigger the `loadMore` callback// when the user scrolls within every items, e.g. 1 - 5, 6 - 10, and so on (default = 15)loadMoreCount:BATCH_COMMENTS,// Provide the loaded state of a batch items to the callback for telling the hook// whether the `loadMore` should be triggered or notisItemLoaded:(loadIndex)=>isItemLoadedArr[loadIndex],// We can fetch the data through the callback, it's invoked when more items need to be loadedloadMore:(e)=>loadData(e,setComments),});return(<divstyle={{width:"300px",height:"500px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, measureRef})=>(<divkey={comments[index]?.id||`fb-${index}`}style={{padding:"16px",minHeight:"122px"}}ref={measureRef}// Used to measure the unknown item size>{comments[index]?.body||"⏳ Loading..."}</div>))}</div></div>);};
import{Fragment,useState}from"react";importuseVirtualfrom"react-cool-virtual";importaxiosfrom"axios";constTOTAL_COMMENTS=500;constBATCH_COMMENTS=5;constisItemLoadedArr=[];// We only have 50 (500 / 5) batches of items, so set the 51th (index = 50) batch as `true`// to avoid the `loadMore` callback from being invoked, yep it's a trick 😉isItemLoadedArr[50]=true;constloadData=async({ loadIndex},setComments)=>{isItemLoadedArr[loadIndex]=true;try{const{data:comments}=awaitaxios(`/comments?postId=${loadIndex+1}`);setComments((prevComments)=>[...prevComments, ...comments]);}catch(err){isItemLoadedArr[loadIndex]=false;loadData({ loadIndex},setComments);}};constLoading=()=><div>⏳ Loading...</div>;constList=()=>{const[comments,setComments]=useState([]);const{ outerRef, innerRef, items}=useVirtual({itemCount:comments.length,// Provide the number of commentsloadMoreCount:BATCH_COMMENTS,isItemLoaded:(loadIndex)=>isItemLoadedArr[loadIndex],loadMore:(e)=>loadData(e,setComments),});return(<divstyle={{width:"300px",height:"500px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.length ?(items.map(({ index, measureRef})=>{constshowLoading=index===comments.length-1&&comments.length<TOTAL_COMMENTS;return(<Fragmentkey={comments[index].id}><divref={measureRef}>{comments[index].body}</div>{showLoading&&<Loading/>}</Fragment>);})) :(<Loading/>)}</div></div>);};
This example demonstrates how to pre-pend items and maintain scroll position for the user.
import{useEffect,useLayoutEffect,useState}from"react";importuseVirtualfrom"react-cool-virtual";importaxiosfrom"axios";constTOTAL_COMMENTS=500;constBATCH_COMMENTS=5;letshouldFetchData=true;letpostId=100;constfetchData=async(postId,setComments)=>{try{const{data:comments}=awaitaxios(`/comments?postId=${postId}`);// Pre-pend new itemssetComments((prevComments)=>[...comments, ...prevComments]);}catch(err){// Try againfetchData(postId,setComments);}};constList=()=>{const[comments,setComments]=useState([]);const{ outerRef, innerRef, items, startItem}=useVirtual({// Provide the number of commentsitemCount:comments.length,onScroll:({ scrollForward, scrollOffset})=>{// Tweak the threshold of data fetching that you wantif(!scrollForward&&scrollOffset<50&&shouldFetchData){fetchData(--postId,setComments);shouldFetchData=false;}},});useEffect(()=>fetchData(postId,setComments),[]);// Execute the `startItem` through `useLayoutEffect` before the browser to paint// See https://reactjs.org/docs/hooks-reference.html#uselayouteffect to learn moreuseLayoutEffect(()=>{// After the list updated, maintain the previous scroll position for the userstartItem(BATCH_COMMENTS,()=>{// After the scroll position updated, re-allow data fetchingif(comments.length<TOTAL_COMMENTS)shouldFetchData=true;});},[comments.length,startItem]);return(<divstyle={{width:"300px",height:"500px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.length ?(items.map(({ index, measureRef})=>(// Used to measure the unknown item size<divkey={comments[index].id}ref={measureRef}>{comments[index].body}</div>))) :(<divclassName="item">⏳ Loading...</div>)}</div></div>);};
When working with filtering items, we can reset the scroll position when theitemCount is changed by enabling theresetScroll option.
import{useState}from"react";importuseVirtualfrom"react-cool-virtual";constList=()=>{const[itemCount,setItemCount]=useState(100);const{ outerRef, innerRef, items}=useVirtual({ itemCount,// Resets the scroll position when the `itemCount` is changed (default = false)resetScroll:true,});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<divkey={index}style={{height:`${size}px`}}> ⭐️{index}</div>))}</div></div>);};
This example demonstrates the scenario of sticking/unsticking the scroll position to the bottom for a chatroom.
import{useState,useEffect}from"react";importuseVirtualfrom"react-cool-virtual";importaxiosfrom"axios";constTOTAL_MESSAGES=200;letisScrolling=false;// Used to prevent UX conflictletid=0;constloadData=async(id,setMessages)=>{try{const{data:messages}=awaitaxios(`/messages/${id}`);setMessages((prevMessages)=>[...prevMessages,messages]);}catch(err){loadData(id,setMessages);}};constChatroom=()=>{const[shouldSticky,setShouldSticky]=useState(true);const[messages,setMessages]=useState([]);const{ outerRef, innerRef, items, scrollToItem}=useVirtual({// Provide the number of messagesitemCount:messages.length,// You can speed up smooth scrollingscrollDuration:50,onScroll:({ userScroll})=>{// If the user scrolls and isn't automatically scrolling, cancel stick to bottomif(userScroll&&!isScrolling)setShouldSticky(false);},});useEffect(()=>{// Mock messages serviceif(id<=TOTAL_MESSAGES)setTimeout(()=>loadData(++id,setMessages),Math.floor(500+Math.random()*2000));},[messages.length]);useEffect(()=>{// Automatically stick to bottom, using smooth scrolling for better UXif(shouldSticky){isScrolling=true;scrollToItem({index:messages.length-1,smooth:true},()=>{isScrolling=false;});}},[messages.length,shouldSticky,scrollToItem]);return(<div><divstyle={{width:"300px",height:"400px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, measureRef})=>(// Used to measure the unknown item size<divkey={`${messages[index].id}`}ref={measureRef}><div>{messages[index].content}</div></div>))}</div></div>{!shouldSticky&&(<buttononClick={()=>setShouldSticky(true)}>Stick to Bottom</button>)}</div>);};
This example demonstrates how to handle input elements (or form fields) in a virtualized list.
import{useState}from"react";importuseVirtualfrom"react-cool-virtual";constdefaultValues=newArray(20).fill(false);constForm=()=>{const[formData,setFormData]=useState({todo:defaultValues});const{ outerRef, innerRef, items}=useVirtual({itemCount:defaultValues.length,});consthandleInputChange=({ target},index)=>{// Store the input values in React statesetFormData((prevData)=>{consttodo=[...prevData.todo];todo[index]=target.checked;return{ todo};});};consthandleSubmit=(e)=>{e.preventDefault();alert(JSON.stringify(formData,undefined,2));};return(<formonSubmit={handleSubmit}><divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<divkey={index}style={{height:`${size}px`}}><inputid={`todo-${index}`}type="checkbox"// Populate the corresponding state to the default valuedefaultChecked={formData.todo[index]}onChange={(e)=>handleInputChange(e,index)}/><labelhtmlFor={`todo-${index}`}>{index}. I'd like to...</label></div>))}</div></div><inputtype="submit"/></form>);};
When dealing with forms, we can useReact Cool Form to handle the form state and boost performance for use.
importuseVirtualfrom"react-cool-virtual";import{useForm}from"react-cool-form";constdefaultValues=newArray(20).fill(false);constForm=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:defaultValues.length,});const{ form}=useForm({defaultValues:{todo:defaultValues},removeOnUnmounted:false,// To keep the value of unmounted fieldsonSubmit:(formData)=>alert(JSON.stringify(formData,undefined,2)),});return(<formref={form}><divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<divkey={index}style={{height:`${size}px`}}><inputid={`todo-${index}`}name={`todo[${index}]`}type="checkbox"/><labelhtmlFor={`todo-${index}`}>{index}. I'd like to...</label></div>))}</div></div><inputtype="submit"/></form>);};
React requireskeys for array items. I'd recommend using an unique id as the key as possible as we can, especially when working with reordering, filtering, etc. Refer tothis article to learn more.
constList=()=>{const{ outerRef, innerRef, items}=useVirtual();return(<divref={outerRef}style={{width:"300px",height:"300px",overflow:"auto"}}><divref={innerRef}>{items.map(({ index, size})=>(// Use IDs from your data as keys<divkey={someData[index].id}style={{height:`${size}px`}}>{someData[index].content}</div>))}</div></div>);};
Server-side rendering allows us to provide a fastFP and FCP, it also benefits forSEO. React Cool Virtual supplies you a seamless DX between SSR and CSR.
constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,ssrItemCount:30,// Renders 0th - 30th items on SSR// OrssrItemCount:[50,80],// Renders 50th - 80th items on SSR});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{/* The items will be rendered both on SSR and CSR, depending on our settings */}{items.map(({ index, size})=>(<divkey={someData[index].id}style={{height:`${size}px`}}>{someData[index].content}</div>))}</div></div>);};
💡 Please note, when using the
ssrItemCount, the initial items will be the SSR items but it has no impact to the UX. In addition, you might notice that some styles (i.e. width, start) of the SSR items are0. It's by design, because there's no way to know the outer's size on SSR. However, you can make up these styles based on the environments if you need.
React Cool Virtual is a custom Reacthook that supplies you withall the features for building highly performant virtualized datasets easily 🚀. It takesoptions parameters and returns useful methods as follows.
constreturnValues=useVirtual(options);
Anobject with the following options:
number
The total number of items. It can be an arbitrary number if actual number is unknown, see theexample to learn more.
number | [number, number]
The number of items that are rendered on server-side, see theexample to learn more.
number | (index: number, width: number) => number
The size of an item (default = 50). When working withdynamic size, it will be the default/or estimated size of the unmeasured items.
- For
numberuse case, please refer to thefixed size example. - For
indexcallback use case, please refer to thevariable size example. - For
widthcallback use case, please refer to theRWD example.
boolean
The layout/orientation of the list (default = false). Whentrue means left/right scrolling, so the hook will usewidth as theitem size and use theleft as thestart position.
boolean
It's used to tell the hook to reset the scroll position when theitemCount is changed (default = false). It's useful forfiltering items.
number
The number of items to render behind and ahead of the visible area (default = 1). That can be used for two reasons:
- To slightly reduce/prevent a flash of empty screen while the user is scrolling. Please note, too many can negatively impact performance.
- To allow the tab key to focus on the next (invisible) item for better accessibility.
boolean
To enable/disable theisScrolling indicator of an item (default = false). It's useful for UI placeholders orperformance optimization when the list is being scrolled. Please note, using it will result in an additional render after scrolling has stopped.
number[]
An array of indexes to make certain items in the list sticky. See theexample to learn more.
- The values must be providedin ascending order, i.e.
[0, 10, 20, 30, ...].
number | (distance: number) => number
The duration ofsmooth scrolling, the unit is milliseconds (default =100ms <= distance * 0.075 <= 500ms).
(time: number) => number
A function that allows us to customize the easing effect ofsmooth scrolling (default =easeInOutSine).
number
How many number of items that you want to load/or pre-load (default = 15), it's used forinfinite scroll. A number 15 means theloadMore callback will be invoked when the user scrolls within every 15 items, e.g. 1 - 15, 16 - 30, and so on.
(index: number) => boolean
A callback for us to provide the loaded state of a batch items, it's used forinfinite scroll. It tells the hook whether theloadMore should be triggered or not.
(event: Object) => void
A callback for us to fetch (more) data, it's used forinfinite scroll. It's invoked when more items need to be loaded, which based on the mechanism ofloadMoreCount andisItemLoaded.
constloadMore=({ startIndex,// (number) The index of the first batch item stopIndex,// (number) The index of the last batch item loadIndex,// (number) The index of the current batch items (e.g. 1 - 15 as `0`, 16 - 30 as `1`, and so on) scrollOffset,// (number) The scroll offset from top/left, depending on the `horizontal` option userScroll,// (boolean) Tells you the scrolling is through the user or not})=>{// Fetch data...};constprops=useVirtual({ loadMore});
(event: Object) => void
This event will be triggered when scroll position is being changed by the user scrolls orscrollTo/scrollToItem methods.
constonScroll=({ overscanStartIndex,// (number) The index of the first overscan item overscanStopIndex,// (number) The index of the last overscan item visibleStartIndex,// (number) The index of the first visible item visibleStopIndex,// (number) The index of the last visible item scrollOffset,// (number) The scroll offset from top/left, depending on the `horizontal` option scrollForward,// (boolean) The scroll direction of up/down or left/right, depending on the `horizontal` option userScroll,// (boolean) Tells you the scrolling is through the user or not})=>{// Do something...};constprops=useVirtual({ onScroll});
(event: Object) => void
This event will be triggered when the size of the outer element changes.
constonResize=({ width,// (number) The content width of the outer element height,// (number) The content height of the outer element})=>{// Do something...};constprops=useVirtual({ onResize});
Anobject with the following properties:
React.useRef<HTMLElement>
Aref to attach to the outer element. We mustapply it for using this hook.
React.useRef<HTMLElement>
Aref to attach to the inner element. We mustapply it for using this hook.
Object[]
The virtualized items for rendering rows/columns. Each item is anobject that contains the following properties:
| Name | Type | Description |
|---|---|---|
| index | number | The index of the item. |
| size | number | The fixed/variable/measured size of the item. |
| width | number | The current content width of the outer element. It's useful for aRWD row/column. |
| start | number | The starting position of the item. We might only need this whenworking with grids. |
| isScrolling | true | undefined | An indicator to show a placeholder oroptimize performance for the item. |
| isSticky | true | undefined | An indicator to make certain items becomesticky in the list. |
| measureRef | Function | It's used to measure an item withdynamic orreal-time heights/widths. |
(offsetOrOptions: number | Object, callback?: () => void) => void
This method allows us to scroll to the specified offset from top/left, depending on thehorizontal option.
// Basic usagescrollTo(500);// Using optionsscrollTo({offset:500,smooth:true,// Enable/disable smooth scrolling (default = false)});
💡 It's possible to customize the easing effect of the smoothly scrolling, see theexample to learn more.
(indexOrOptions: number | Object, callback?: () => void) => void
This method allows us to scroll to the specified item.
// Basic usagescrollToItem(10);// Using optionsscrollToItem({index:10,// Control the alignment of the item, acceptable values are: "auto" (default) | "start" | "center" | "end"// Using "auto" will scroll the item into the view at the start or end, depending on which is closeralign:"auto",// Enable/disable smooth scrolling (default = false)smooth:true,});
💡 It's possible to customize the easing effect of the smoothly scrolling, see theexample to learn more.
(index: number, callback?: () => void) => void
This method is used to work withpre-pending items. It allows us to main the previous scroll position for the user.
Items are re-rendered whenever the user scrolls. If your item is aheavy data component, there're two strategies for performance optimization.
UseReact.memo
When working withnon-dynamic size, we can extract the item to it's own component and wrap it withReact.memo. It shallowly compares the current props and the next props to avoid unnecessary re-renders.
import{memo}from"react";importuseVirtualfrom"react-cool-virtual";constMemoizedItem=memo(({ height, ...rest})=>{// A lot of heavy computing here... 🤪return(<div{...rest}style={{height:`${height}px`}}> 🐳 Am I heavy?</div>);});constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,itemSize:75,});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, size})=>(<MemoizedItemkey={index}height={size}/>))}</div></div>);};
If the above solution can't meet your case or you're working withdynamic size. React Cool Virtual supplies you anisScrolling indicator that allows you to replace the heavy component with a light one while the user is scrolling.
import{forwardRef}from"react";importuseVirtualfrom"react-cool-virtual";constHeavyItem=forwardRef((props,ref)=>{// A lot of heavy computing here... 🤪return(<div{...props}ref={ref}> 🐳 Am I heavy?</div>);});constLightItem=(props)=><div{...props}>🦐 I believe I can fly...</div>;constList=()=>{const{ outerRef, innerRef, items}=useVirtual({itemCount:1000,useIsScrolling:true,// Just use it (default = false)// OruseIsScrolling:(speed)=>speed>50,// Use it based on the scroll speed (more user friendly)});return(<divstyle={{width:"300px",height:"300px",overflow:"auto"}}ref={outerRef}><divref={innerRef}>{items.map(({ index, isScrolling, measureRef})=>isScrolling ?(<LightItemkey={index}/>) :(<HeavyItemkey={index}ref={measureRef}/>))}</div></div>);};
💡 Well... the
isScrollingcan also be used in many other ways, please use your imagination 🤗.
You can share aref as follows, here we take theouterRef as the example:
import{useRef}from"react";importuseVirtualfrom"react-cool-virtual";constApp=()=>{constref=useRef();const{ outerRef}=useVirtual();return(<divref={(el)=>{outerRef.current=el;// Set the element to the `outerRef`ref.current=el;// Share the element for other purposes}}/>);};
React Cool Virtual is designed tosimplify the styling and keep all the items in the document flow for rows/columns. However, when working with grids, we need to layout the items in two-dimensional. For that reason, we also provide thestart property for you to achieve it.
import{Fragment}from"react";importuseVirtualfrom"react-cool-virtual";constGrid=()=>{constrow=useVirtual({itemCount:1000,});constcol=useVirtual({horizontal:true,itemCount:1000,itemSize:100,});return(<divstyle={{width:"400px",height:"400px",overflow:"auto"}}ref={(el)=>{row.outerRef.current=el;col.outerRef.current=el;}}><divstyle={{position:"relative"}}ref={(el)=>{row.innerRef.current=el;col.innerRef.current=el;}}>{row.items.map((rowItem)=>(<Fragmentkey={rowItem.index}>{col.items.map((colItem)=>(<divkey={colItem.index}style={{position:"absolute",height:`${rowItem.size}px`,width:`${colItem.size}px`,// The `start` property can be used for positioning the itemstransform:`translateX(${colItem.start}px) translateY(${rowItem.start}px)`,}}> ⭐️{rowItem.index},{colItem.index}</div>))}</Fragment>))}</div></div>);};
React Cool Virtual is built withTypeScript, you can tell the hook what type of yourouter andinner elements are as follows.
If the outer element and inner element are the different types:
constApp=()=>{// 1st is the `outerRef`, 2nd is the `innerRef`const{ outerRef, innerRef}=useVirtual<HTMLDivElement,HTMLUListElement>();return(<divref={outerRef}><ulref={innerRef}>{/* Rendering items... */}</ul></div>);};
If the outer element and inner element are the same types:
constApp=()=>{// By default, the `innerRef` will refer to the type of the `outerRef`const{ outerRef, innerRef}=useVirtual<HTMLDivElement>();return(<divref={outerRef}><divref={innerRef}>{/* Rendering items... */}</div></div>);};
💡 For more available types, pleasecheck it out.
ResizeObserver has good support amongst browsers, but it's not universal. You'll need to use polyfill for browsers that don't support it. Polyfills is something you should do consciously at the application level. Therefore React Cool Virtual doesn't include it.
We recommend using@juggle/resize-observer:
$ yarn add @juggle/resize-observer# or$ npm install --save @juggle/resize-observerThen pollute thewindow object:
import{ResizeObserver}from"@juggle/resize-observer";if(!("ResizeObserver"inwindow))window.ResizeObserver=ResizeObserver;
You could use dynamic imports to only load the file when the polyfill is required:
(async()=>{if(!("ResizeObserver"inwindow)){constmodule=awaitimport("@juggle/resize-observer");window.ResizeObserver=module.ResizeObserver;}})();
- Support window scrolling
- Leverage the power ofOffscreen API (maybe...)
💡 If you have written any blog post or article about React Cool Virtual, please open a PR to add it here.
- Featured onReact Status #243.
- Featured onReact Newsletter #270.
Thanks goes to these wonderful people (emoji key):
Welly 🤔💻📖🚇🚧 | Nikita Pilgrim 💻 | Jie Peng 📖 | Alex Lyakhnitskiy 💻 | Adam Pash 📖 |
This project follows theall-contributors specification. Contributions of any kind welcome!
About
😎 ♻️ A tiny React hook for rendering large datasets like a breeze.
Topics
Resources
License
Code of conduct
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Sponsor this project
Uh oh!
There was an error while loading.Please reload this page.