Infinite paginations & filter using supabase and react query

Setting up infinite paginations with tanstack

  ·   5 min read

I’ve been furiously working on zimchu.com. A rental listing site for apartments in Thimphu. I ended up using supabase for it and also used react-query for all of the fetch stuff.

The biggest assumption I’m making for this project is that people want a more organized and easily searchable data for house listings. So a filter feature is a must have for the mvp.

I wanted an infinite scroll for the rental listing page. react-query already has hooks and examples for infinite pagination so that made the initial setup relatively easy. The confusing portion was trying to combine the filter form with the infinite pagination.

Infinite Pagination Setup

This is the infinite pagination query from the react-query-docs.

const {
    data,
    error,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery('projects',async ({pageParam}) => fetchProjects(pageParam), {
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

Most of it was copy pasta except configuring the nextPageParam and hasNextPage with supabase. The docs, quite rightfully, show it for actual BE APIs.

The getNextPageParam decides both pageParam and hasNextPage variables. If you return undefined in getNextPageParam it sets hasNextPage to false. If you return anything other than undefined it gets passed to your fetch function as pageParam.

My supabase query at this point looked something like this

const fetchRentals = async () => {
    supabase.from('listings').select()
}

I needed to change supabase query to implement pagination using pageParam and it looked something like this

const fetchRentals = async (pageParam: number) => {
    const PAGE_SIZE = 5
    const from = pageParam * PAGE_SIZE
    const to = (pageParam + 1) * PAGE_SIZE - 1
    return supabase.from('listings').select().sort({ascending: false}).from(from, to)
}

Okay great! At this point I had set up pagination for supabase and it was quite smooth sailing. Then I moved on to implementing getNextPageParam function so that it would automatically load more listings once the user is at the end of the page

The code looked something like this. This was also quite straightforward other than the part where I had to figure out I needed to return undefined if theres no more data. That just took time reading the docs though.

const {
    data,
    error,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery('projects',async ({pageParam = 0}) => fetchProjects(pageParam), {
    // lastPage is the result of the last fetch call
    getNextPageParam: (lastPage, pages) => {
      // 5 because i set the pagination result limit to 5, if less than 5 i know theres no more data
        if (lastPage.length < 5) {
            return undefined
        }
        // otherwise i return this, which is the pageParam
        return pages.length + 1
    },
  })

initally pageParam will be undefined so you have to set it to zero for the initial query

Filter and Infinite Pagination

Great! I implemented basic infinite query listing with supabase and react-query but now I had the tricky part of setting it up with the filters. I wanted to make sure that everytime I set a filter, the pageParam reset to zero and the supabase call was executed with the filters otherwise this would be useless.

For the filter I had a form which had the fields minRent, maxRent, size,location.

The key to implementing this was with how QueryKeys worked(pun intended). QueryKeys get converted to arrays and in the above case projects string in useInfiniteQuery gets converted to ['projects']. And you can also add other things to the array. Say any params we use can be added to this: ['projects', anyParamHere] and this is passed on to the function we have in useInfiniteQuery.

The form is build using react-hook-form.

const init = {minRent: '' , maxRent: '', size: '', location: ''}
const {getValues} = useForm({defaultValues: init})
const {
    data,
    error,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery('projects',async ({pageParam = 0}) => fetchProjects(pageParam), {
    getNextPageParam: (lastPage, pages) => {
        if (lastPage.length < 5) {
            return undefined
        }
        return pages.length + 1
    },
  })

I wanted to set up the queryKeys such that it also took the form and everytime the form changed I wanted the infinite query to run.

So I set it up like this:

const init = {minRent: '' , maxRent: '', size: '', location: ''}
const {getValues} = useForm({defaultValues: init})
const [form, setForm] = useState(getValues())
const {
    data,
    error,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery(['projects',form],async ({pageParam = 0}) => fetchProjects(pageParam, form), {
    getNextPageParam: (lastPage, pages) => {
        if (lastPage.length < 5) {
            return undefined
        }
        return pages.length + 1
    },
  })

And updated the supabase fetch call to use the form to build up the query instead of executing it at once

const fetchRentals = async (pageParam: number, { minRent, maxRent, size, location}) => {
    const PAGE_SIZE = 5
    const from = pageParam * PAGE_SIZE
    const to = (pageParam + 1) * PAGE_SIZE - 1
    let query = supabase.from('listings').select().sort({ascending: false}).from(from, to)
    query = query.gte('rent', !minRent ? 0 : minRent )
    query = query.lte('rent', !maxRent ? 100000 : maxRent ) 
    if (size) {
        query = query.eq('size', size)
    }
    if (location) {
        query = query.eq('location', location)
    }
    return query
}

While writing the blog I wanted to check if it could work using a ref instead of a state value since I dont use it for rendering purposes and it seems to work. Updated code:

const init = {minRent: '' , maxRent: '', size: '', location: ''}
const {getValues} = useForm({defaultValues: init})
const form = useRef(form)
const {
    data,
    error,
    hasNextPage,
    fetchNextPage,
  } = useInfiniteQuery(['projects',form.current],async ({pageParam = 0}) => fetchProjects(pageParam, form.current), {
    getNextPageParam: (lastPage, pages) => {
        if (lastPage.length < 5) {
            return undefined
        }
        return pages.length + 1
    },
  })

I have it set up such that when the user hits the apply button i set the form vals to be whatever the user entered.