Skip to content

Instantly share code, notes, and snippets.

@ivandoric
Created May 30, 2021 09:28
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ivandoric/2f770c7b8c165d76a431e34c98312d76 to your computer and use it in GitHub Desktop.
Save ivandoric/2f770c7b8c165d76a431e34c98312d76 to your computer and use it in GitHub Desktop.
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])
}
@arihantverma
Copy link

Understood, thank you

@kasem-sm
Copy link

kasem-sm commented Nov 8, 2022

Hi. I guess you are missing something. The data fetched from Server Side rendering is of no use until you pass staleTime to react query as the docs says that "If you configure your query observer with initialData, and no staleTime (the default staleTime: 0), the query will immediately refetch when it mounts:"

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