Skip to main content
3 of 3
added 152 characters in body

Search Result with infinite scroll React component

I've coded a React component which should display search result and give the user the option to scroll down and retrieve 25 more objects to be displayed when the scrollbar hits the bottom. What I've noticed when I compile and test the component is that the filter buttons to mark is really slow: it takes a couple of seconds after I click on it till it gets marked, and the same applies for bookmark buttons. The component feels really sluggish when it loads in. However, for each time more items are loaded in, it also generates more publishedStates for the publishedButton. I notice that as when it gets generated over 1000 states and 1000 objects is loaded, when I click on buttons to change the state on filter etc is slow.

How can I improve and optimize the performance of my component?

import * as React from "react";
import * as _ from "lodash";
declare const $: any

interface Props {
    userId: number;
}

interface State {
    objects: Object | null
    isVisible: Boolean
    searchString: string
    bookmarkStates: Array<boolean>
    publishedStates: Array<boolean>
    categories: Array<String>
    languages: Array<String>
    published: Array<String>
    hasMoreObjects: Boolean,
    storiesLoaded: number
}

const getStateName = function (isFiltered, state) {
    if (JSON.stringify(isFiltered)==JSON.stringify(state.commodityStates))
        return 'commodityStates';
    else if (JSON.stringify(isFiltered)==JSON.stringify(state.languageStates))
        return 'languageStates';
    else
        return 'publishedStates';
}

const handlePublishedTime = function (publishedTime) {
    let publishedTimeCopy = publishedTime.split(' ')
    let monthNames = [ "January", "February", "March", "April", "May", "June", 
                        "July", "August", "September", "October", "November", "December" ];
    let todayDate = new Date();
    let dd = todayDate.getDate();
    let mm = todayDate.getMonth();
    let yyyy = todayDate.getFullYear();
    // Used for comparison of dates.
    let todayDateFormat = dd + ', ' +  monthNames[mm] + ' ' + yyyy;     
    let publishedTimeFormat =   publishedTimeCopy[2].split(',') + ' ' + publishedTimeCopy[3] + ' ' + publishedTimeCopy[4]; 

    if (todayDateFormat == publishedTimeFormat) {
        return publishedTimeCopy[0];
    }

    return publishedTimeCopy[2].replace(',',' ') + ' ' + Resources[publishedTimeCopy[3]].substring(0,3)
}

export default class SearchResult extends React.PureComponent<Props, State> {  
    constructor(props) {
        super(props);

        this.state = {
            Object: null,
            isVisible: true,
            searchString: "",
            bookmarkStates: [],
            commodityStates: [],
            languageStates: [],
            publishedStates: [],
            categories: [],
            languages: [],
            published: ['1D', '2D', '1W', '1Y', '5Y'],
            hasMoreObjects: true,
            objectsLoaded: 25
        }

        $.get('/webapi/test, function (data) {
            this.setState({
                Object: data
            });
        }.bind(this));
    }

    handleScroll = (e) => {
        const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
        if (bottom) {  
            this.setState({
                storiesLoaded: this.state.storiesLoaded + 25
            });
        }
    }

    handleSearchTextInput (event) {
        this.setState({
            searchString: event
        });
    }

    handleSearchButton () {
        this.setState({
            isVisible: true
        });
    }

    addNewBookmark = function (userId, newsId, isBookmarked, bookmarkIndex) {
        $.post('/webapi/newstestAddNewBookmark?userId=' + (userId), 
        { 
            UserID: userId,
        })
        .done(function () {
            this.changeBookmarkState(isBookmarked, bookmarkIndex);
        }.bind(this))   
    }
    
    removeBookmark = function (userId, newsId, isBookmarked, bookmarkIndex) {
        $.post('/webapi/test/RemoveBookmark?userId=' + (userId), {})
        .done(function () {
            this.changeBookmarkState(isBookmarked, bookmarkIndex);
        }.bind(this))
    }

    changeBookmarkState(isBookmarked, index) {
        let bookmarkStatesCopy = this.state.bookmarkStates.slice(); // Create a copy to avoid mutation.
        bookmarkStatesCopy[index] = isBookmarked == true ? false : true;
        
        this.setState({
            bookmarkStates: bookmarkStatesCopy
        });
    }

    changeFilterState(filterByState, index) {
        const stateName = getStateName(filterByState, this.state);
        let filterByStateCopy = filterByState.slice(); // Create a copy to avoid mutation.

        if (index == 0) {
            filterByStateCopy[index] = filterByState[index] === false;
            if (stateName != 'publishedStates')
                filterByStateCopy.map(({}, index1) => {
                    filterByStateCopy[index1+1] = false;
                });
        }
        else {
            filterByStateCopy[index] = filterByState[index] === false;
            stateName != 'publishedStates' ? filterByStateCopy[0] = false : null
        };

        this.setState({
            [stateName]: filterByStateCopy
        } as any);
    }

    renderBookmarkButton(userId, newsId, isBookmarked, bookmarkIndex) {  
        return (
            <a onClick={()=>{isBookmarked == false ? this.addNewBookmark(userId, newsId, isBookmarked, bookmarkIndex) : this.removeBookmark(userId, newsId, isBookmarked, bookmarkIndex);}}>
                <img className={isBookmarked == false ? "bookmark-button" : "bookmark-button bookmark-button-fill-color"} src="/images/logo/Bookmark.svg" />
            </a>
        );
    }

    renderCategories(Object , isFiltered) {   
        let categories = this.state.categories;
        
        Object.map((object) => {
            if (Object == null)
                return null;

            object.Categories.map((categoryObj, index) => {
                if (categories[index] == null)
                    categories.push(categoryObj)
                    if(index === 0)
                        categories.splice(0, 0, 'All');         
            });
        });

        // Used to create a new array which contains all elements that pass the test.
        categories = categories.filter((value, index, array) => 
             !array.filter((v, i) => JSON.stringify(value) == JSON.stringify(v) && i < index).length);

        const mappedCategories = categories.map((category, index) => {
            if (isFiltered[index] == null) {
                isFiltered[index] = false; 
                if (isFiltered[0] == false) {
                    isFiltered[0] = true;    
                }
            };
                       
            return (
                <div key={index}>
                    <a className="checkbox-display" onClick={()=>{ this.changeFilterState(isFiltered, index);}}>
                    {isFiltered[index] ? 
                        <div className={ isFiltered[index] ? "checkbox-rectangle checkbox-rectangle-fill-color horizontal-row-filterby" : "checkbox-rectangle horizontal-row-filterby"}>
                            <img className="checkbox-image-size" src="/images/logo/checkmark.svg"/>
                        </div> : 
                        <div className="checkbox-rectangle horizontal-row-filterby"/>
                    }
                    </a>
                        <span className="category1">{index == 0 ? 'All' : category['Name']}</span>              
                </div>        
            );
        });

        return (
            <div>
                <div className="filter-by-commodity">{Resources.Commodity}</div>
                {mappedCategories}
           </div>
        );       
    }

    renderLanguage(Object , languageFilter) {   
        let languages = this.state.languages;

        Object.map((object ) => {
            if (Object == null)
                return null;

            object.Languages.map((languageObj, index) => {         
                if (languages[index] == null){
                    languages.push(languageObj)
                    if(index === 0){
                        languages.splice(0, 0, 'All');
                    };
                };
            });
        });

        // Used to create a new array which contains all elements that pass the test.
        languages = languages.filter((value, index, array) => 
             !array.filter((v, i) => JSON.stringify(value) == JSON.stringify(v) && i < index).length);

        const mappedLanguages = languages.map((language, index) => {
            if (languageFilter[index] == null) {
                languageFilter[index] = false; 
                if (languageFilter[0] == false) {
                    languageFilter[0] = true;
                }
            };
                
            return (
                <div key={index}>
                    <a onClick={()=>{ this.changeFilterState(languageFilter, index);}}>
                    {languageFilter[index] ? 
                        (<div className={languageFilter[index] ? "checkbox-rectangle checkbox-rectangle-fill-color horizontal-row-filterby" : "checkbox-rectangle horizontal-row-filterby"}>
                            <img className="checkbox-image-size" src="/images/logo/checkmark.svg"/>
                        </div>) : <div className="checkbox-rectangle horizontal-row-filterby"/>
                    }
                    </a>
                        <span className="category1">{index == 0 ? 'All' : language['LanguageName']}</span>              
                </div>        
            );
        });

        return (
            <div>
                <div className="horizonal-line-short" />
                <div className="filter-by-commodity">{Resources.Languages}</div>
                {mappedLanguages}
            </div>
        );
    }

    renderPublished(publishedFilter) {
        let published = this.state.published;

        const mappedPublished =
            published.map((pushlish , index) => {
                if (publishedFilter[index] == null) 
                        publishedFilter[index] = (false); 
                
                return (
                    <div key={index}>
                        <a onClick={()=>{ this.changeFilterState(publishedFilter, index);}}>
                                <div className={publishedFilter[index] ? "checkbox-rectangle-large vertical-row-filterby checkbox-rectangle-fill-color" : "checkbox-rectangle-large vertical-row-filterby"}>
                                    <span>{pushlish}</span>    
                                </div>
                        </a>          
                    </div>        
                );
            });

        return (
            <div>
                <div className="horizonal-line-short" />
                <div className="filter-by-commodity">{Resources.Published}</div>     
                <div className='checkbox-display'> {mappedPublished}</div>     
            </div>
        );
    }

    renderStories(Object ) {
        let isBookmarked = this.state.bookmarkStates;

        if(Object == null)
            return null;

        const mappedStories = 
            Object.slice(0, this.state.objectsLoaded).map((object , index) =>  {
                if (isBookmarked[index] == null) 
                    isBookmarked[index] = object.IsBookmarkedMain;
                
                return (
                    <div key={index} className={index == 0 ? "object-rectangle-no-bg" : index % 2 == 0 ? 'object-rectangle-no-bg' : 'object-rectangle'}>
                        <span className="published-time">{handlePublishedTime(object.PublishedTime)}  </span>
                        <div>
                            <a className="object-header" href={"/test/object.aspx?newsId=" + object.ID}>{object.Header}</a>
                            {this.renderBookmarkButton(this.props.userId, object.Id, isBookmarked[index], index)}
                        </div>
                    </div>
                );   
            });

        return (
            <div>
                {mappedStories}
            </div>
        );
    }

    renderResults(Object, isVisible, commodityStates, languageStates, publishedStates) {
        if (isVisible){
            return (
                <div className="search-result-master-container">
                    <span className="section-header">{Resources.Filter_By}</span>
                    <span className="section-header">{Object.length} {Resources.Results}:</span>
                    <div id="filterBySideBar">
                        <div className="horizontal-line-filter">
                            <div>{this.renderCategories(Object , commodityStates)}</div>
                            <div>{this.renderLanguage(Object , languageStates)}</div>
                            <div>{this.renderPublished(publishedStates)}</div>
                        </div>
                    </div>
                    <div className="horizontal-line-stories-top"/>
                    <div className="scrollbar"  onScroll={this.handleScroll.bind(this)}>
                        <div className="force-overflow">
                                {this.renderStories(Object)}
                        </div>
                    </div>
                    <div className="horizontal-line-stories-bottom" />
                </div>    
            );       
        }                 
    }

    render() {
        let Object = this.state.Object;
        let isVisbible = this.state.isVisible;
        let commodityStates = this.state.commodityStates;
        let languageStates = this.state.languageStates;
        let publishedStates = this.state.publishedStates;

        if (Object == null)
            return null;

        let renderShowResults = this.renderResults(Object , isVisbible, commodityStates, languageStates, publishedStates);

        return renderShowResults
    };
}