Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Infinite Scroll And Filters With React Query
import Container from "components/ui/Container"
import VideoCard from "components/VideoCard"
import fetchData from "helpers/fetchData"
import { useEffect, useState, Fragment, useRef } from "react"
import { useInfiniteQuery } from "react-query"
import useIntersectionObserver from "../hooks/useIntersectionObserver"
import Select from "react-select"
import { useUIDSeed } from "react-uid"
import { useRouter } from "next/router"
import Seo from "components/Seo"
import type { GetServerSideProps } from "next"
import type { Video } from "types"
interface Tag {
id: number | string
name: string
slug: string
}
interface AllVideosProps {
videos: {
pages: [
{
posts: [Video]
},
]
pageParams: [number | undefined]
}
tags: [Tag]
}
interface QueryKeyType {
pageParam: number
queryKey: [[string]]
}
const getMoreVideos = async ({ pageParam = 1, queryKey }: QueryKeyType) => {
const [tags] = queryKey
const tagsQueryString = tags.join(",")
if (tagsQueryString !== "") {
const videos = await fetchData(`/tags/${tagsQueryString}?page=${pageParam}`)
return videos
}
const videos = await fetchData(`/all-videos?page=${pageParam}`)
return videos
}
function AllVideos({ videos, tags }: AllVideosProps) {
const seed = useUIDSeed()
const router = useRouter()
const [tagIds, setTagIds] = useState([])
const { data, isSuccess, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteQuery(
[tagIds],
getMoreVideos,
{ getNextPageParam: (page) => (page.current_page === page.last_page ? undefined : page.current_page + 1) },
{ initialData: videos },
)
const loadMoreRef = useRef()
useIntersectionObserver({
target: loadMoreRef,
onIntersect: fetchNextPage,
enabled: hasNextPage,
})
return (
<Container>
<Seo
title="Browse all tutorials"
currentUrl={router.asPath}
description="Browse all tutorials"
imageUrl="/images/default-image.jpg"
/>
<h2 className="my-8 lg:my-20 text-2xl md:text-4xl lg:text-6xl font-bold">Browse All Tutorials</h2>
<div className="mb-8 bg-gray-50 p-4 inline-block w-full md:w-1/3">
<div className="w-full">
<Select
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
options={tags}
isMulti
placeholder="Filter by tag"
instanceId="tags"
onChange={(values) => setTagIds(values.map((tag) => tag.id))}
/>
</div>
</div>
<div className="md:flex md:flex-wrap md:justify-between">
{isSuccess &&
data?.pages.map((page) => (
<Fragment key={seed(page)}>
{page.data.map((video: Video) => (
<VideoCard key={video.id} video={video} />
))}
</Fragment>
))}
</div>
<div ref={loadMoreRef} className={`${!hasNextPage ? "hidden" : ""}`}>
{isFetchingNextPage ? "Loading more..." : ""}
</div>
{isLoading && (
<div className="text-center bg-gray-50 p-8 rounded-md text-gray-400 text-xl mt-14">
Loading videos! ❤️
</div>
)}
{!hasNextPage && !isLoading && (
<div className="text-center bg-gray-50 p-8 rounded-md text-gray-400 text-xl mt-14">
Congrats! You have scrolled through all the tutorials. You rock! 🤘
</div>
)}
</Container>
)
}
export const getServerSideProps: GetServerSideProps = async () => {
const data = await fetchData("/all-videos")
const tags = await fetchData("/tags")
const videos = {
pages: [{ data }],
pageParams: [null],
}
return {
props: {
videos,
tags,
},
}
}
export default AllVideos
import "styles/globals.css"
import type { AppProps } from "next/app"
import { ThemeProvider } from "@emotion/react"
import Header from "components/Header"
import Footer from "components/Footer"
import theme from "theme/theme"
import { QueryClientProvider, QueryClient } from "react-query"
const queryClient = new QueryClient()
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider theme={theme}>
<Header />
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
<Footer />
</ThemeProvider>
)
}
export default MyApp
function fetchData(url: string) {
const apiurl = process.env.API_URL ? process.env.API_URL : process.env.NEXT_PUBLIC_API_URL
const data = fetch(`${apiurl}${url}`).then((res) => res.json())
return data
}
export default fetchData
import { useEffect } from "react"
export default function useIntersectionObserver({
enabled = true,
onIntersect,
root,
rootMargin = "0px",
target,
threshold = 0.1,
}) {
useEffect(() => {
if (!enabled) {
return
}
const observer = new IntersectionObserver(
(entries) => entries.forEach((entry) => entry.isIntersecting && onIntersect()),
{
root: root && root.current,
rootMargin,
threshold,
},
)
const el = target && target.current
if (!el) {
return
}
observer.observe(el)
return () => {
observer.unobserve(el)
}
}, [target.current, enabled])
}
@ivandoric
Copy link
Author

ivandoric commented May 30, 2021

Code used in: Infinite Scroll And Filters With React Query. Check out the video here

@arihantverma
Copy link

arihantverma commented Jun 28, 2021

Hey @ivandoric! Thank you for the code and the video. I have one question:

https://gist.github.com/ivandoric/2f770c7b8c165d76a431e34c98312d76#file-all-tsx-L99

if isSuccess is false for say page 3 fetching, wouldn't it also hide all the previously fetched data of page 1 and 2?

@ivandoric
Copy link
Author

ivandoric commented Jun 28, 2021

@arihantverma I think this depends on error, but needs to be tested. If the page cannot be fetched I think that the other items will stay on page. However I know for a fact that if an item is missing some property and you are not handling that case (ie. defining some sort of fallback) then you will get an error message in dev mode and 500 Internal server error in production.

You also have error available in the status so you can also check that and display error message if you get an error.

@arihantverma
Copy link

arihantverma commented Jun 28, 2021

Understood, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment