Skip to content

Commit

Permalink
feat(fe): implement infinite scroll (#1532)
Browse files Browse the repository at this point in the history
* feat: set up providers and install react query

* feat: add useInview, edit fetch method

* feat: implement infinite scroll with DataSet Type

* feat: add isLoadButton state

* feat: convert to suspenseinfinitequery

* fix: fix return value to apply suspense

* fix: return isFetchingNextPage

* fix: fix take value

* feat: add react-query

* feat: implement problem infinite table

* feat: replace problemtable with probleminfinitetable

* chore: change provider location

* chore: remove previous problem table
  • Loading branch information
youznn authored Mar 15, 2024
1 parent b909a12 commit c9f4796
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import DataTable from '@/components/DataTable'
import SearchBar from '@/components/SearchBar'
import { Skeleton } from '@/components/ui/skeleton'
import { useInfiniteScroll } from '@/lib/useInfiniteScroll'
import type { Problem } from '@/types/type'
import { useSearchParams } from 'next/navigation'
import { columns } from './Columns'

export default function ProblemInfiniteTable() {
const searchParams = useSearchParams()
const search = searchParams.get('search') ?? ''
const order = searchParams.get('order') ?? 'id-asc'
const newSearchParams = new URLSearchParams()
newSearchParams.set('search', search)
newSearchParams.set('order', order)

const { items, total, ref, isFetchingNextPage } = useInfiniteScroll<Problem>({
pathname: 'problem',
query: newSearchParams
})

return (
<>
<div className="flex items-center justify-between text-gray-500">
<div className="flex gap-1">
<p className="text-2xl font-bold text-gray-500">All</p>
<p className="text-2xl font-bold text-blue-500">{total}</p>
</div>
<SearchBar />
</div>
<div className="flex flex-col items-center">
<DataTable
data={items}
columns={columns}
headerStyle={{
title: 'text-left w-5/12',
difficulty: 'w-2/12',
submissionCount: 'w-2/12',
acceptedRate: 'w-2/12',
info: 'w-1/12'
}}
linked
/>
{isFetchingNextPage && (
<>
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="my-2 flex h-12 w-full rounded-xl" />
))}
</>
)}
<div ref={ref} />
</div>
</>
)
}
40 changes: 0 additions & 40 deletions apps/frontend/app/(main)/problem/_components/ProblemTable.tsx

This file was deleted.

80 changes: 39 additions & 41 deletions apps/frontend/app/(main)/problem/page.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
import SearchBar from '@/components/SearchBar'
'use client'

import { Skeleton } from '@/components/ui/skeleton'
import type { Problem } from '@/types/type'
import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
import { Suspense } from 'react'
import ProblemTable from './_components/ProblemTable'

interface ProblemProps {
searchParams: { search: string; tag: string; order: string }
}

export default function Problem({ searchParams }: ProblemProps) {
const search = searchParams?.search ?? ''
const order = searchParams?.order ?? 'id-asc'
import ProblemInfiniteTable from './_components/ProblemInfiniteTable'

export default function Problem() {
const queryClient = new QueryClient()
return (
<>
<div className="flex justify-end text-gray-500">
<SearchBar />
</div>
<Suspense
fallback={
<>
<div className="mt-4 flex">
<span className="w-5/12">
<Skeleton className="h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-1/12">
<Skeleton className="mx-auto h-6 w-12" />
</span>
</div>
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="my-2 flex h-12 w-full rounded-xl" />
))}
</>
}
>
<ProblemTable search={search} order={order} />
</Suspense>
<QueryClientProvider client={queryClient}>
<Suspense
fallback={
<>
<div className="mt-4 flex">
<span className="w-5/12">
<Skeleton className="h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-2/12">
<Skeleton className="mx-auto h-6 w-20" />
</span>
<span className="w-1/12">
<Skeleton className="mx-auto h-6 w-12" />
</span>
</div>
{[...Array(5)].map((_, i) => (
<Skeleton
key={i}
className="my-2 flex h-12 w-full rounded-xl"
/>
))}
</>
}
>
<ProblemInfiniteTable />
</Suspense>
</QueryClientProvider>
</>
)
}
118 changes: 118 additions & 0 deletions apps/frontend/lib/useInfiniteScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useInView } from 'react-intersection-observer'
import type { URLSearchParams } from 'url'
import { fetcher, fetcherWithAuth } from './utils'

interface Item {
id: number
}

interface DataSet<T> {
problems: T[] //TODO: 백엔드 get류 api return type 통일되면 problems -> data로 고치기
total: number
}

interface UseInfiniteScrollProps {
pathname: string
query: URLSearchParams
itemsPerPage?: number
withAuth?: boolean
}

/**
*
* @param pathname
* The path or endpoint from which data will be fetched.
*
* @param query
* The URLSearchParams object representing the query parameters for data fetching.
*
* @param itemsPerPage
* The number of items to fetch per page. Default is 5.
*
* @param withAuth
* A boolean indicating whether authentication is required for data fetching. Default is false.
*
*/

export const useInfiniteScroll = <T extends Item>({
pathname,
query,
itemsPerPage = 10,
withAuth = false
}: UseInfiniteScrollProps) => {
//fetch datas with pageParams and url
const getInfiniteData = async ({
pageParam
}: {
pageParam?: number
}): Promise<DataSet<T>> => {
if (!query.has('take')) query.append('take', String(itemsPerPage))
pageParam && pageParam > 0 && query.set('cursor', pageParam.toString())
let dataSet: DataSet<T>
withAuth
? (dataSet = await fetcherWithAuth
.get(pathname, {
searchParams: query
})
.json())
: (dataSet = await fetcher
.get(pathname, {
searchParams: query
})
.json())
return dataSet
}

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useSuspenseInfiniteQuery({
queryKey: [pathname, query.toString()],
staleTime: 0,
queryFn: getInfiniteData,
initialPageParam: 0,
getNextPageParam: (lastPage: DataSet<T>) => {
return lastPage?.problems.length === 0 || //TODO: fix problem to data
lastPage?.problems.length < itemsPerPage
? undefined //페이지에 있는 아이템 수가 0이거나 itemsPerPage보다 작으면 undefined를 반환합니다.
: lastPage.problems.at(-1)?.id //cursor를 getData의 params로 넘겨줍니다.
}
})

const { ref, inView } = useInView()

useEffect(() => {
if (inView && !isFetchingNextPage && hasNextPage) {
fetchNextPage()
}
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage, data])

/*
5번 이상 바닥에 닿으면 자동 페칭을 멈추고, loadmore 버튼을 보이게 합니다.
const scrollCounter = useRef(0) // 바닥에 닿은 횟수를 세는 카운터
const [isLoadButton, setIsLoadButton] = useState(false)
useEffect(() => {
if (inView && !isFetchingNextPage) {
if (hasNextPage) {
if (scrollCounter.current < 5) {
setIsLoadButton(false)
fetchNextPage()
scrollCounter.current += 1
} else {
setIsLoadButton(true)
}
}
} else setIsLoadButton(false)
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage, data])
*/

return {
items: data.pages.flat().flatMap((page) => page.problems),
total: data.pages.at(0)?.total,
fetchNextPage,
ref,
isFetchingNextPage
}
}
2 changes: 2 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.106.0",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.27.5",
"@tanstack/react-table": "^8.13.2",
"@tiptap/core": "^2.2.4",
"@tiptap/extension-link": "^2.2.4",
Expand Down Expand Up @@ -69,6 +70,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
"react-icons": "^5.0.1",
"react-intersection-observer": "^9.8.1",
"react-resizable-panels": "^2.0.12",
"react-use": "^17.5.0",
"sharp": "^0.33.2",
Expand Down
Loading

0 comments on commit c9f4796

Please sign in to comment.