Skip to content

Commit

Permalink
refactor(fe): refactor infinite scroll with tanstack query (#2244)
Browse files Browse the repository at this point in the history
* feat(fe): add problem infinite list query option

* feat(fe): add intersection area component

* refactor(fe): refactor problem infinite table

* feat(fe): add tanstack query error boundary

* fix(fe): pass itemsPerPage parameter

* chore(fe): use common error fallback ui
  • Loading branch information
eunnbi authored Nov 29, 2024
1 parent eae7736 commit 4ab66a9
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 180 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use client'

import DataTable from '@/app/(client)/(main)/_components/DataTable'
import { problemQueries } from '@/app/(client)/_libs/queries/problem'
import IntersectionArea from '@/components/IntersectionArea'
import { Skeleton } from '@/components/shadcn/skeleton'
import { useInfiniteScroll } from '@/libs/hooks/useInfiniteScroll'
import type { Problem } from '@/types/type'
import { useSuspenseInfiniteQuery } from '@tanstack/react-query'
import { useSearchParams } from 'next/navigation'
import SearchBar from '../../_components/SearchBar'
import { columns } from './Columns'
Expand All @@ -12,46 +13,57 @@ 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
})
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useSuspenseInfiniteQuery(problemQueries.infiniteList({ search, order }))

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>
<p className="text-2xl font-bold text-blue-500">{data.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) => (
<IntersectionArea
disabled={!hasNextPage || isFetchingNextPage}
onIntersect={fetchNextPage}
>
<DataTable
data={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} />
</IntersectionArea>
</div>
</>
)
}

export function ProblemInfiniteTableFallback() {
return (
<>
<div className="mt-4 flex">
<span className="w-5/12">
<Skeleton className="h-6 w-20" />
</span>
</div>
{[...Array(5)].map((_, i) => (
<Skeleton key={i} className="my-2 flex h-12 w-full rounded-xl" />
))}
</>
)
}
45 changes: 11 additions & 34 deletions apps/frontend/app/(client)/(main)/problem/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,16 @@
'use client'

import { Skeleton } from '@/components/shadcn/skeleton'
import type { Problem } from '@/types/type'
import FetchErrorFallback from '@/components/FetchErrorFallback'
import { TanstackQueryErrorBoundary } from '@/components/TanstackQueryErrorBoundary'
import { Suspense } from 'react'
import ProblemInfiniteTable from './_components/ProblemInfiniteTable'
import ProblemInfiniteTable, {
ProblemInfiniteTableFallback
} from './_components/ProblemInfiniteTable'

export default function Problem() {
export default function ProblemListPage() {
return (
<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>
<TanstackQueryErrorBoundary fallback={FetchErrorFallback}>
<Suspense fallback={<ProblemInfiniteTableFallback />}>
<ProblemInfiniteTable />
</Suspense>
</TanstackQueryErrorBoundary>
)
}
48 changes: 48 additions & 0 deletions apps/frontend/app/(client)/_libs/queries/infiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
infiniteQueryOptions,
type QueryKey,
type UnusedSkipTokenInfiniteOptions
} from '@tanstack/react-query'

interface Item {
id: number
}

interface DataSet<T extends Item> {
data: T[]
total: number
}

type PageParam = number | undefined

interface GetInfiniteQueryOptionsParams<T extends Item, K extends QueryKey>
extends Partial<
UnusedSkipTokenInfiniteOptions<DataSet<T>, Error, DataSet<T>, K, PageParam>
> {
queryKey: K
itemsPerPage?: number
}

export const getInfiniteQueryOptions = <T extends Item, K extends QueryKey>({
queryFn,
queryKey,
itemsPerPage = 10
}: GetInfiniteQueryOptionsParams<T, K>) => {
return infiniteQueryOptions({
queryKey,
queryFn,
staleTime: 0,
initialPageParam: undefined,
getNextPageParam: (lastPage) => {
return lastPage.data.length === 0 || lastPage.data.length < itemsPerPage
? undefined //페이지에 있는 아이템 수가 0이거나 itemsPerPage보다 작으면 undefined를 반환합니다.
: lastPage.data.at(-1)?.id //cursor를 getData의 params로 넘겨줍니다.
},
select: (data) => {
const items = data.pages.flat().flatMap((page) => page.data)
const total = data.pages.at(0)?.total

return { items, total }
}
})
}
16 changes: 16 additions & 0 deletions apps/frontend/app/(client)/_libs/queries/problem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getProblemList, type GetProblemListRequest } from '../apis/problem'
import { getInfiniteQueryOptions } from './infiniteQuery'

export const problemQueries = {
all: () => ['problem'] as const,
infiniteList: (params: GetProblemListRequest) =>
getInfiniteQueryOptions({
queryKey: [...problemQueries.all(), 'list'] as const,
queryFn: ({ pageParam }) =>
getProblemList({
...params,
...(pageParam ? { cursor: pageParam } : undefined)
}),
itemsPerPage: params.take
})
}
29 changes: 29 additions & 0 deletions apps/frontend/components/IntersectionArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, type ReactNode } from 'react'
import { useInView } from 'react-intersection-observer'

interface IntersectionAreaProps {
children: ReactNode
disabled: boolean
onIntersect: () => void
}

export default function IntersectionArea({
children,
disabled,
onIntersect
}: IntersectionAreaProps) {
const { ref, inView } = useInView()

useEffect(() => {
if (inView && !disabled) {
onIntersect()
}
}, [inView, disabled])

return (
<>
{children}
<div ref={ref} />
</>
)
}
28 changes: 28 additions & 0 deletions apps/frontend/components/TanstackQueryErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client'

import {
ErrorBoundary,
type ErrorBoundaryFallbackProps
} from '@suspensive/react'
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import type { FunctionComponent, ReactNode } from 'react'

interface TanstackQueryErrorBoundaryProps {
children: ReactNode
fallback: FunctionComponent<ErrorBoundaryFallbackProps<Error>>
}

export function TanstackQueryErrorBoundary({
children,
fallback
}: TanstackQueryErrorBoundaryProps) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallback={fallback}>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
118 changes: 0 additions & 118 deletions apps/frontend/libs/hooks/useInfiniteScroll.ts

This file was deleted.

0 comments on commit 4ab66a9

Please sign in to comment.