Skip to content

Instantly share code, notes, and snippets.

@dhruva81
Forked from ivandoric/all.tsx
Created June 1, 2024 18:08
Show Gist options
  • Save dhruva81/25b1ae3c94ed643b317094d72678cfdb to your computer and use it in GitHub Desktop.
Save dhruva81/25b1ae3c94ed643b317094d72678cfdb 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])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment