Using RTK Query to accomplish a classic Load-More page, I don't know what is the right way to do it?

Viewed 935

API:

import {
  createApi,
  fetchBaseQuery,
} from '@reduxjs/toolkit/query/react'
import { RootState } from 'store'

export interface FeedType {
  id: string
  title: string
  imgUrl: string
}

export const feedsApi = createApi({
  reducerPath: 'feeds',
  tagTypes: ['Feeds'],
  baseQuery: fetchBaseQuery({
    baseUrl: 'http://localhost:5000',
  }),
  endpoints: (build) => ({
    getFeedsMore: build.query<FeedType[], void>({
      async queryFn(arg, queryApi, extraOptions, baseQuery) {
        const state = queryApi.getState() as RootState
        const selector = feedsApi.endpoints.getFeedsMore.select() as (
          state: any
        ) => any
        const result = selector(state) as { data: FeedType[] } | undefined
        const oldData = (result?.data ?? []) as FeedType[]
        const { data } = await baseQuery({
          url: 'feeds?_page=' + Math.round(oldData.length / 10 + 1),
        })
        return { data: [...oldData, ...(data as FeedType[])] }
      },
    }),
  }),
})

export const {
  useGetFeedsMoreQuery,
} = feedsApi

Component:

import FeedItem from 'components/FeedItem'
import React from 'react'
import Masonry from 'react-masonry-css'
import { useGetFeedsMoreQuery } from 'services/feeds'

interface FeedsMorePageProps {}

const FeedsMorePage: React.FunctionComponent<FeedsMorePageProps> = () => {
  const { isLoading, data: feeds, refetch } = useGetFeedsMoreQuery()
  return (
    <>
      {isLoading ? (
        'loading'
      ) : (
        <>
          <Masonry
            breakpointCols={{
              default: 3,
              1100: 2,
              700: 1,
            }}
            className="my-masonry-grid"
            columnClassName="my-masonry-grid_column"
          >
            {feeds &&
              feeds.map((feed) => <FeedItem key={feed.id} feed={feed} />)}
          </Masonry>
          <button className="btn btn-info" onClick={() => refetch()}>
            Load More...
          </button>
        </>
      )}
    </>
  )
}

export default FeedsMorePage

I know it is totally a mess, this is the only clumsy way I could make it run.

What is the best practice to this scenario?

It is common to use dispatch and getState in a Thunk-Action, but the most confusing part to me is in queryFn I have getState and endpoint.select methods, but I do not know how to type them in typescript.

feedsApi.endpoints.getFeedsMore.select()(state as RootState)
// this gives me a super long type incompatible complain

I can not use useSelector hook here neither, util I made out this ugly way...

1 Answers

Generally, no, that's not what you should do. Building one cache entry that large will mean that eventually you will run out of memory. It can never be collected, so it will just keep growing. But if the user scrolled down to item 9500, you really don't need to keep item 1000 in memory any more.

Especially when you are displaying all those elements in your DOM. And if you are not displaying all those elements in the DOM, there is also no need to have all of them in the cache.

So, assume you use some kind of virtualization library like react-virtual.

That means you know you have theoretically 10000 items to display, but you only render what is in view and a bit to the front and a bit to the back.

Then keep your endpoint also to a window. Have your endpoint fetch "parts", so if the user is looking at item 9500, you maybe have items 9500-9550 on the screen and you want to keep some more fetched to quickly display them - one page to the front and one to the back.

So now you use your query hook three times in your component: the current "page" (9500-9550), the last "page" (9450-9500) and the next "page" (9550-9600).

That way, stuff not in view can at some point be cache-collected if it was not in view long enough.

Another way of doing that would be to just render "page" components from a parent component - and each of those "page" components would request their "window of data", while a "get more" button would add another element to the "pages" array in the parent component.

But either way: you would not stitch all of that together in the cache, but keep it as separate cache entries - and then decide to access which of those to access in your component and how to stitch them together.

Generally, I can recommend to read up on this GitHub discussion where multiple people share their approaches to the topic.

Related