SPFx Infinite Scroll

Introduction

 
SharePoint Framework is gaining popularity, being utilized by so many developers for client-side customization in SharePoint.
 
In this article, we will learn how to use SharePoint Online Search APIs and the use of Lazy loading to display in the SPFx web part.
 

Overview

 
Today, we are going to have a demo calling the SPO Out-Of-The-Box search APIs along with the help of 3rd party library named WayPoint to display the data as an Infinite scroll.
 
The source code that shipped with this article has usage of re-usable code by having a Helper file, calling Parent methods from a child component, usage of PnP search. 
 
This targets advanced users. Therefore, for this demo, one should be familiar with the following topics:
  • React JS
  • Search APIs
  • PnP Libraries
  • Use of React libraries in SPFx web parts
Below is a sample image that you see when the web part is rendered:
 
 
This assumes you know how to create a simple React SPFx web part. If you are first-timer, you can follow this article to get an idea of how to create a React-based web part. Please note that you can do this without a Javascript framework also.
 

Create WebPart

 
Let's jump in to technical work. Start creating a webpart with the following options:
  • solution name - aadi-spfx-react-wp
  • webpart name - SearchSite
  • Install PnP library dependencies
To create the control as lazy loading, we need to install the third-party library WayPoint by typing the npm command: 
  1. npm install react-waypoint --save  
In the SearchSite.tsx, declare a State to hold our values:
  1. interface ISearchSiteState {  
  2.   txtSearch: string;  
  3.   queryText: string;  
  4.   startRow: number;  
  5.   rowLimit: number;  
  6.   searchResults: SearchResults;  
  7.   searchMainResults: ISearchMainResults[];  
  8. }  
Set the deafult values in the constructor of the class:
  1. public constructor(props: ISearchSiteProps) {  
  2.     super(props);  
  3.     this.state = {  
  4.       txtSearch: "",  
  5.       queryText: "",  
  6.       startRow: 0,  
  7.       rowLimit: 10,  
  8.       searchResults: null,  
  9.       searchMainResults: []  
  10.     };  
  11.   }  
Finally, there's the render method for the initial controls, say, a text box for a keyword to search, and a button to hit SPO, search APIs, and display the initial results:
  1. public render(): React.ReactElement<ISearchSiteProps> {  
  2.     return (  
  3.       <div className={styles.searchSite}>  
  4.         {this.renderSearchbox()}  
  5.         {this.state.searchResults  
  6.           ? <div className={styles.container}>  
  7.             <div className={styles.row}>  
  8.               <div className={styles.column}>  
  9.                 <span className={styles.title}>SharePoint search!!</span>  
  10.                 <p className={styles.subTitle}>SharePoint search using PnP.</p>  
  11.                 <p className={styles.description}>Keyword: {escape(this.state.queryText)}</p>  
  12.                 <p className={styles.description}>Count: {this.state.searchResults.TotalRows}</p>  
  13.                 <p className={styles.description}>ElapsedTime: {this.state.searchResults.ElapsedTime}</p>  
  14.               </div>  
  15.             </div>  
  16.             <div className={styles.row}>  
  17.               {this.state.searchResults.TotalRows > 0  
  18.                 ? <MainResults  
  19.                   key={this.state.txtSearch}  
  20.                   searchResults={this.state.searchMainResults}  
  21.                   hasMoreItems={this.state.startRow < this.state.searchResults.TotalRows}  
  22.                   fetchMore={() => { this.fetchMoreResults(); }}>  
  23.                 </MainResults>  
  24.                 : <div className={styles.column}>No rows found.</div>}  
  25.             </div>  
  26.           </div>  
  27.           : null}  
  28.       </div>  
  29.     );  
  30.   }  
  31.   
  32. private renderSearchbox(): JSX.Element {  
  33.     return (  
  34.       <div className={styles.container}>  
  35.         <div className={styles.row}>  
  36.           <div className={styles.column}>  
  37.             <TextField id="txtSearch" onChange={this.setSearchTextValue}></TextField>  
  38.             <DefaultButton onClick={this.btnSearchText}>Search</DefaultButton>  
  39.           </div>  
  40.         </div>  
  41.       </div>);  
  42.   }  
  43.   
  44.  @autobind  
  45.   private setSearchTextValue(e) {  
  46.     this.setState({  
  47.       txtSearch: e.target.value  
  48.     });  
  49.   }  
  50.   
  51. @autobind  
  52.   private btnSearchText() {  
  53.     this.setState({  
  54.       searchResults: null,  
  55.       searchMainResults: [],  
  56.       startRow: 0  
  57.     }, async () => {  
  58.       await this.getSearchResults();  
  59.     });  
  60.   }  
Observe a custom component, MainResults, is a child component which displays the search results by calling a resuable method from a Helper file, which is in the source code zip file.
 
The below code is responsible for fetching the search items with the given key text. By default, we will retrieve only 10 items (startRow) at once, but you can change the count in State if you wish. 
  1. private static getSearchItems(k: string, startRow: number, rowLimit: number): Promise<SearchResults> {  
  2.         return new Promise<SearchResults>((resolve, reject) => {  
  3.             const query: SearchQueryInit = {  
  4.                 Querytext: k,  
  5.                 TrimDuplicates: true,  
  6.                 EnableInterleaving: true,  
  7.                 StartRow: startRow,  
  8.                 RowLimit: rowLimit,  
  9.                 Properties: [  
  10.                     {  
  11.                         Name: "EnableDynamicGroups",  
  12.                         Value: { QueryPropertyValueTypeIndex: QueryPropertyValueType.BooleanType, BoolVal: true }  
  13.                     },  
  14.                     {  
  15.                         Name: "EnableMultiGeoSearch",  
  16.                         Value: { QueryPropertyValueTypeIndex: QueryPropertyValueType.BooleanType, BoolVal: true }  
  17.                     },  
  18.                 ],  
  19.                 SelectProperties: ["Title""Author""Path""Description""FileExtension""SiteId""WebId"],  
  20.                 SortList: [  
  21.                     { Property: "Author", Direction: SortDirection.Ascending }  
  22.                 ]  
  23.             };  
  24.   
  25.             sp.search(query)  
  26.                 .then((r: SearchResults) => {  
  27.                     resolve(r);  
  28.                 }, e => { console.error(e); reject(e); });  
  29.         });  
  30.     }  
In the child class component: MainResults, we need to have only the Props interface, since we need to load the data fetched from the parent component.
 
Note
When the parent's data is passed to the child (as props), and whenever there is a change of data in the parent component, only the props variable data get affected in the render method of child component, but not it's (Child) state data. That's why in the child component, I never created a State interface.
 
The child's component class render methd: renderSeachResults() displays the first 10 records from the parent, as shown below:
  1. public render(): React.ReactElement<IMainResultsProps> {  
  2.         return (<React.Fragment> 
  3.             {this.renderSearchResults()}  
  4.             {this.renderWayPoint()}  
  5.         </React.Fragment>);  
  6.     }  
  7.   
  8.     private renderSearchResults = (): React.ReactElement => {  
  9.         return (<>  
  10.             {this.props.searchResults.map((r, k) => {  
  11.                 return (  
  12.                     <div className={styles.column}>  
  13.                         <p className={styles.description}>Result number: {k + 1}</p>  
  14.                         <p className={styles.description}>Title: {r.Title}</p>  
  15.                         {r.Description  
  16.                             ? <p className={styles.description}>Description: {r.Description}</p>  
  17.                             : null}  
  18.                         <p className={styles.description}>Author: {r.Author}</p>  
  19.                         <p className={styles.description}>Path: {r.Path}</p>  
  20.                         <p className={styles.description}>FileExtension: {r.FileExtension}</p>  
  21.                     </div>  
  22.                 );  
  23.             })  
  24.             }  
  25.         </>);  
  26.     }  
When the user scrolls and reaches to the end of the 10th item, an event notifies us saying it reached the last item and more items are to be fetched from it's parent component. That's where we use the WayPoint library to be notified.
  1. private renderWayPoint = (): React.ReactElement => {  
  2.         return (  
  3.             this.props.hasMoreItems  
  4.                 ? <div className={styles.column}>  
  5.                     <Waypoint  
  6.                         onEnter={this.handleWaypointEnter}>  
  7.                         <div>Loading...</div>  
  8.                     </Waypoint>  
  9.                 </div>  
  10.                 : <React.Fragment />  
  11.         );  
  12.     }  
  13.   
  14.     private handleWaypointEnter = (): void => {  
  15.           this.props.fetchMore();
  16.     }  
After reaching the end of the scroll bar, the props variable "hasMoreItems" checks if it has got additional items, if so, it triggers the method "handleWaypointEnter" to fetch more items from it's parent. This is a very good usage of calling re-usable methods and also in calling parent's method from a child component. 
 
Run the following command, and browse to the workbench.aspx page. Start typing "Pages" in the search box and hit the Search button to view the results.
  1. gulp serve --nobrowser  
This completes our demo. You can view the items that are loaded again. At the end of the scroll, it re-loads again. 
 

Conclusion

 
In this article, we learned how to fetch the items using Search API, and render those as a Lazy load or Infinite scroll manner.
 
We learned how to call Parent's method from its child component.
 
Finally, we saw how to have a Helper class and use it across multiple web parts, if you have any.
 
Feel free to download the source code zip file, unzip it, and run 'npm i' to load the dependency packages.