Created
May 30, 2021 09:28
-
-
Save ivandoric/2f770c7b8c165d76a431e34c98312d76 to your computer and use it in GitHub Desktop.
Infinite Scroll And Filters With React Query
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | |
} |
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
Understood, thank you