Infinity scroll +Virtualization using the example of ReactJS+ RTK Query

Today, any self-respecting enterprise, be it a construction goods store or a company providing business services, all strive to “put” their goods and services on the Internet. This is understandable – we live in an age of rapidly developing technologies and more than 65% of the world’s population (about 5.3 billion people) have access to the Internet, and by 2025 this number will increase to 6.54 billion (impressive, isn’t it?). So, what am I talking about, they all need to be served, they all need to be offered services, goods, etc. As the saying goes: “There is no friend according to taste and color,” and it’s true that there are so many people – so many opinions, and in our case, goods and services. Against this background, a reasonable question arises: “How can I display all this on my website, so that the user does not wait until next year for the site page to load, when by that time there will be more products that need to be loaded?” With such a picture of the world and the most optimistic forecasts about the pace of the emergence of new things, we are careless to enter into some kind of recursion.

Since childhood, we have been taught to eat in small portions and chew thoroughly, so why, in the current situation, should we receive all the information not in one batch, but in portions? This is exactly the solution I propose to consider in my article. And if we touch on the topic of food (apparently, we shouldn’t pee on an empty stomach), then we should swallow food that we have already chewed, and not accumulate it in our mouth, otherwise someday it will burst (Joker, I have no complaints about you). Likewise, we will remove elements from the DOM tree that are not visible to the user, so as not to overload our site.

The technologies that I chose, or rather, that the employer chose for me to complete the test task: React, RTK Query. My thirst for knowledge and cognition, as well as the desire to write, forced me to go a little further, so below I will compare the rendering time with infinite scrolling with virtualization and if we loaded all our data at once.

Let’s move on to implementation. Our experimental data will be posts from https://jsonplaceholder.typicode.com. We create an application using the create-react-app command, install RTK Query (I have: “@reduxjs/toolkit”: “^1.9.5”). We wrap our root component in a provider and move on to setting up the store.

Create an api:

export const postApi=createApi({
    reducerPath:'post',
    baseQuery:fetchBaseQuery({baseUrl:'https://jsonplaceholder.typicode.com'}),
    endpoints:(build)=>({
        fetchAllPosts: build.query<IPost[],{limit:number,start:number}>({
            query:({limit=5, start=0 })=>({
                url:'/posts',
                params:
                 {
                    _limit:limit,
                    _start:start,
                }
            })
        }),
        fetchPostById: build.query<IPost,number>({
            query:(id:number=1)=>({
                url:`/posts/${id}`,
            })
        })
    })
})

We pass it to rootReducer and define the setupStore function, which will install the store for the provider:

const rootReducer= combineReducers({
    [postApi.reducerPath]:postApi.reducer
})

export const setupStore=()=>{
    return configureStore({
        reducer:rootReducer,
        middleware:(getDefaultMidleware)=> getDefaultMidleware().concat(postApi.middleware)
    })
}

Index.tsx

const store=setupStore()
const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
     <App/>
  </Provider>
);

Let’s create our component for one post:

interface IPostItemProps{
    post:IPost
}
const PostItem:FC<IPostItemProps> = ({post}) => {
    const navigate=useNavigate()
    return (
        <div className="container__postItem">
            <div>№ {post.id}</div>
            <div className="postitem__title">Title: {post.title}</div>
            <div  className="postitem__body">
              Body:  {post.body.length>20?post.body.substring(0,20)+'...':post.body}
            </div>
        </div>
    );
};

Let’s move directly to the logic of rendering our components.

Let’s define two states in the post container: one to determine the moment when the scroll has reached the top of the page, the other when it has reached the bottom. And also the hook that RTK Query generated for us, where we pass our limit (number of posts) and starting index (index of the first post):

const [isMyFetching,setIsFetchingDown]=useState(false)
 const [isMyFetchingUp,setIsMyFetchingUp]=useState(false)
 const {data:posts, isLoading} =postApi.useFetchAllPostsQuery({limit:7,start:currentPostStart})

Let’s create a function that will calculate when the top or bottom has been reached and return the scroll to the middle position:

const scrollHandler=(e:any):void=>{
        if(e.target.documentElement.scrollTop<50)
        {
            setIsMyFetchingUp(true)
        }
        if(e.target.documentElement.scrollHeight-e.target.documentElement.scrollTop-window.innerHeight<50)
        {
            setIsFetchingDown(true)
            window.scrollTo(0,(e.target.documentElement.scrollHeight + e.target.documentElement.scrollTop));
        }
    }

Where

e.target.documentElement.scrollHeight – the height of the entire scroll;

e.target.documentElement.scrollTop – how much we have already scrolled from the top;

window.innerHeight – the height of the visible part of the page.

Then, during the first rendering of this component, you need to attach event listeners to the scroll and remove them when unmounting, so as not to accumulate them:

useEffect(()=>{
  document.addEventListener('scroll',scrollHandler)
  return ()=>{
    document.removeEventListener('scroll',scrollHandler)
  }
},[])

Let’s define a useEffect hook that fires when the bottom of the screen is reached:

useEffect(()=>{
  if(isMyFetching)
  {
      setCurrentPostStart(prev=>{
          return prev<93?prev+1:prev
      })
      setIsFetchingDown(false)  
     
  }
},[isMyFetching])

It is worth noting here that when changing the starting index, we work with the previous value and if it is already less than 93, that is, we have reached a certain maximum (JSONPlaceholder only provides us with 100 posts), then we return the current value, otherwise we increase the index by one.

We do the same when we reach the top of the page:

useEffect(()=>{
    if(isMyFetchingUp)
    {
        setCurrentPostStart(prev=>{
            return prev>0?prev-1:prev
        })
        setIsMyFetchingUp(false)  
       
    }
  },[isMyFetchingUp])

Code for the entire component:

const PostContainer: FC = () => {
    const [currentPostStart,setCurrentPostStart]=useState(0)
    const {data:posts, isLoading} =postApi.useFetchAllPostsQuery({limit:7,start:currentPostStart})
    const [isMyFetching,setIsFetchingDown]=useState(false)
    const [isMyFetchingUp,setIsMyFetchingUp]=useState(false)
    useEffect(()=>{
        if(isMyFetching)
        {
            setCurrentPostStart(prev=>{
                return prev<93?prev+1:prev
            })
            setIsFetchingDown(false)  
        }
    },[isMyFetching])
    useEffect(()=>{
    if(isMyFetchingUp)
    {
        setCurrentPostStart(prev=>{
            return prev>0?prev-1:prev
        })
        setIsMyFetchingUp(false)  
    }
    },[isMyFetchingUp])
    useEffect(()=>{
      document.addEventListener('scroll',scrollHandler)
      return ()=>{
        document.removeEventListener('scroll',scrollHandler)
      }
    },[])
    const scrollHandler=(e:any):void=>{
        if(e.target.documentElement.scrollTop<50)
        {
            setIsMyFetchingUp(true)
        }
        if(e.target.documentElement.scrollHeight-e.target.documentElement.scrollTop-window.innerHeight<50)
        {
            setIsFetchingDown(true)
            window.scrollTo(0,(e.target.documentElement.scrollHeight + e.target.documentElement.scrollTop));
        }
    }
    return (
        <div>
            <div className="post__list">
                {posts?.map(post=><PostItem key={post.id} post={post}/>)}
            </div>
            {isLoading&&<div>Загрузка данных</div>}
        </div>
    );
};

It’s time to experiment.

Let’s look at an example where we download all 100 posts with one request. The total render time, which is displayed in the Profiler tab in React DevTools, in this case was 44.1 ms.

If you upload 7 posts in portions, the time will be reduced to 23.2 ms.

In addition to saving time before the first rendering of the content, we also gain in performance due to the fact that we do not need to store all elements in the DOM, but only those that are visible to the user on the screen.

If you look at the figure below, you can see that the number of nodes displaying posts remains constant at 7.

Also, when scrolling up, requests for previous posts do not occur, because RTK Query caches them, which again allows you to increase performance.

In conclusion of the article, we can conclude that the proposed implementation allowed us to gain time in the first rendering of content – one of the Google PageSpeed ​​Insights metrics that affects the ranking of a site on the Internet. Also, endless scrolling + virtualization is a worthy alternative to pagination and other technologies that involve issuing information in portions.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *