Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a cursor-based infinite scroll example #4775

Open
wants to merge 1 commit into
base: feature/infinite-query-integration
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/query/react/infinite-queries/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/aa419c22/@reduxjs/toolkit/_pkg.tgz",
"@reduxjs/toolkit": "https://pkg.csb.dev/reduxjs/redux-toolkit/commit/87270fe3/@reduxjs/toolkit/_pkg.tgz",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intersection-observer": "^9.13.1",
Expand Down
54 changes: 39 additions & 15 deletions examples/query/react/infinite-queries/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
import { BrowserRouter, Link, Route, Routes, useLocation } from "react-router"
import "./App.css"
import { BrowserRouter, Routes, Route, Link } from "react-router"

import { PaginationExample } from "./features/pagination/PaginationExample"
import { Outlet } from "react-router"
import BidirectionalCursorInfScroll from "./features/bidirectional-cursor-infinite-scroll/BidirectionalCursorInfScroll"
import {
InfiniteScrollExample,
InfiniteScrollAbout,
InfiniteScrollExample,
} from "./features/infinite-scroll/InfiniteScrollExample"
import { InfiniteScrollMaxPagesExample } from "./features/max-pages/InfiniteScrollMaxExample"
import { PaginationExample } from "./features/pagination/PaginationExample"

const Menu = () => {
return (
<div>
<h2>Examples</h2>
<ul>
<li>
<Link to="/pagination">Pagination</Link>
<Link to="/examples/pagination">Pagination</Link>
</li>
<li>
<Link to="/infinite-scroll">Infinite Scroll</Link>
<Link to="/examples/infinite-scroll">Infinite Scroll</Link>
</li>
<li>
<Link to="/infinite-scroll-max">Infinite Scroll + max pages</Link>
<Link to="/examples/infinite-scroll-max">
Infinite Scroll + max pages
</Link>
</li>
<li>
<Link to="/examples/bidirectional-cursor-infinte-scroll">
Bidirectional Cursor-Based Infinite Scroll
</Link>
</li>
</ul>
</div>
Expand All @@ -32,18 +41,33 @@ const App = () => {
<BrowserRouter>
<div className="App">
<h1>RTKQ Infinite Query Example Showcase</h1>

<Routes>
<Route path="/" element={<Menu />} />
<Route path="/pagination" element={<PaginationExample />} />
<Route path="/infinite-scroll" element={<InfiniteScrollExample />} />
<Route
path="/infinite-scroll/about"
element={<InfiniteScrollAbout />}
/>
<Route
path="/infinite-scroll-max"
element={<InfiniteScrollMaxPagesExample />}
/>
path="/examples"
element={
<div>
<Link to="/">Back to Menu</Link>
<Outlet />
</div>
}
>
<Route path="pagination" element={<PaginationExample />} />
<Route path="infinite-scroll" element={<InfiniteScrollExample />} />
<Route
path="infinite-scroll/about"
element={<InfiniteScrollAbout />}
/>
<Route
path="infinite-scroll-max"
element={<InfiniteScrollMaxPagesExample />}
/>
<Route
path="bidirectional-cursor-infinte-scroll"
element={<BidirectionalCursorInfScroll />}
/>
</Route>
</Routes>
</div>
</BrowserRouter>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useCallback, useEffect, useRef, useState } from "react"
import { Link, useLocation } from "react-router"
import { apiWithInfiniteScroll } from "./infiniteScrollApi"

const limit = 10

function BidirectionalCursorInfScroll({ startingProject = { id: 25 } }) {
const {
hasPreviousPage,
hasNextPage,
data,
error,
isFetching,
isLoading,
isError,
fetchNextPage,
fetchPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} =
apiWithInfiniteScroll.endpoints.getProjectsBidirectionalCursor.useInfiniteQuery(
limit,
{
initialPageParam: {
around: startingProject.id,
limit,
},
},
)

const beforeRef = useIntersectionObserver({
isFetching,
callback: fetchPreviousPage,
})
const afterRef = useIntersectionObserver({
isFetching,
callback: fetchNextPage,
})

const location = useLocation()

const startingProjectRef = useRef<HTMLDivElement>(null)
const [hasCentered, setHasCentered] = useState(false)

useEffect(() => {
if (hasCentered) return
const startingElement = startingProjectRef.current
if (startingElement) {
startingElement.scrollIntoView({
behavior: "auto",
block: "center",
})
setHasCentered(true)
}
}, [data?.pages, hasCentered])

return (
<div>
<h2>Bidirectional Cursor-Based Infinite Scroll</h2>
{isLoading ? (
<p>Loading...</p>
) : isError ? (
<span>Error: {error.message}</span>
) : null}
<>
<div>
<button
onClick={() => fetchPreviousPage()}
disabled={!hasPreviousPage || isFetchingPreviousPage}
>
{isFetchingPreviousPage
? "Loading more..."
: hasPreviousPage
? "Load Older"
: "Nothing more to load"}
</button>
</div>
<div
style={{
overflow: "auto",
margin: "1rem 0px",
height: "400px",
}}
>
<div ref={beforeRef}></div>
{data?.pages.map(page => (
<React.Fragment key={page.pageInfo?.endCursor}>
{page.projects.map((project, index, arr) => {
return (
<div
style={{
margin: "1em 0px",
border: "1px solid gray",
borderRadius: "5px",
padding: "2rem 1rem",
background: `hsla(${project.id * 30}, 60%, 80%, 1)`,
}}
key={project.id}
ref={
project.id === startingProject.id
? startingProjectRef
: null
}
>
<div>{`Project ${project.id} (created at: ${project.createdAt})`}</div>
<div>{`Server Time: ${page.serverTime}`}</div>
</div>
)
})}
</React.Fragment>
))}
<div ref={afterRef}></div>
</div>
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? "Loading more..."
: hasNextPage
? "Load Newer"
: "Nothing more to load"}
</button>
</div>
<div>
{isFetching && !isFetchingPreviousPage && !isFetchingNextPage
? "Background Updating..."
: null}
</div>
</>

<hr />
<Link to="/infinite-scroll/about" state={{ from: location.pathname }}>
Go to another page
</Link>
</div>
)
}

export default BidirectionalCursorInfScroll

const useIntersectionObserver = ({
isFetching,
callback,
}: {
isFetching: boolean
callback: () => void
}) => {
const observerRef = useRef<IntersectionObserver | null>(null)

const observerCallback = useCallback(
(node: HTMLDivElement | null) => {
if (isFetching) return
if (observerRef.current) observerRef.current.disconnect()

observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) {
callback()
}
})

if (node) observerRef.current.observe(node)
},
[isFetching, callback],
)

return observerCallback
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { baseApi } from "../baseApi"

type Project = {
id: number
createdAt: string
}

type ProjectsCursorPaginated = {
projects: Project[]
serverTime: string
pageInfo: {
startCursor: number
endCursor: number
hasNextPage: boolean
hasPreviousPage: boolean
}
}

interface ProjectsInitialPageParam {
before?: number
around?: number
after?: number
limit: number
}
type QueryParamLimit = number

export const apiWithInfiniteScroll = baseApi.injectEndpoints({
endpoints: builder => ({
getProjectsBidirectionalCursor: builder.infiniteQuery<
ProjectsCursorPaginated,
QueryParamLimit,
ProjectsInitialPageParam
>({
query: ({ before, after, around, limit }) => {
const params = new URLSearchParams()
params.append("limit", String(limit))
if (after != null) {
params.append("after", String(after))
} else if (before != null) {
params.append("before", String(before))
} else if (around != null) {
params.append("around", String(around))
}

return {
url: `https://example.com/api/projectsBidirectionalCursor?${params.toString()}`,
}
},
infiniteQueryOptions: {
initialPageParam: { limit: 10 },
getPreviousPageParam: (
firstPage,
allPages,
firstPageParam,
allPageParams,
) => {
if (!firstPage.pageInfo.hasPreviousPage) {
return undefined
}
return {
before: firstPage.pageInfo.startCursor,
limit: firstPageParam.limit,
}
},
getNextPageParam: (
lastPage,
allPages,
lastPageParam,
allPageParams,
) => {
if (!lastPage.pageInfo.hasNextPage) {
return undefined
}
return {
after: lastPage.pageInfo.endCursor,
limit: lastPageParam.limit,
}
},
},
}),
}),
})
Loading