Skip to content

Commit

Permalink
Add infinite query examples (#4775)
Browse files Browse the repository at this point in the history
* Add bidirectional cursor infinite scroll example

* Add example using limit and offset
- Add example using page and size
- add an intersection observer callback ref hook

* Bump infinite query RTK version

---------

Co-authored-by: Mark Erikson <[email protected]>
  • Loading branch information
remus-selea and markerikson authored Dec 31, 2024
1 parent c9cc8ca commit 78ff764
Show file tree
Hide file tree
Showing 11 changed files with 847 additions and 22 deletions.
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/c9cc8ca0/@reduxjs/toolkit/_pkg.tgz",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intersection-observer": "^9.13.1",
Expand Down
71 changes: 56 additions & 15 deletions examples/query/react/infinite-queries/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
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 LimitOffsetExample from "./features/limit-offset/LimitOffsetExample"
import { InfiniteScrollMaxPagesExample } from "./features/max-pages/InfiniteScrollMaxExample"
import PaginationInfScrollExample from "./features/pagination-infinite-scroll/PaginationInfScrollExample"
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="/examples/infinite-scroll">Infinite Scroll</Link>
</li>
<li>
<Link to="/infinite-scroll">Infinite Scroll</Link>
<Link to="/examples/infinite-scroll-max">
Infinite Scroll + max pages
</Link>
</li>
<li>
<Link to="/infinite-scroll-max">Infinite Scroll + max pages</Link>
<Link to="/examples/bidirectional-cursor-infinte-scroll">
Bidirectional Cursor-Based Infinite Scroll
</Link>
</li>
<li>
<Link to="/examples/limit-offset">
Limit and Offset Infinite Scroll
</Link>
</li>
<li>
<Link to="/examples/pagination-infinite-scroll">
Pagination Infinite Scroll
</Link>
</li>
</ul>
</div>
Expand All @@ -32,18 +53,38 @@ 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 path="limit-offset" element={<LimitOffsetExample />} />
<Route
path="pagination-infinite-scroll"
element={<PaginationInfScrollExample />}
/>
</Route>
</Routes>
</div>
</BrowserRouter>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useCallback, useRef } from "react"

export function useIntersectionCallback(onIntersectCallback: () => void) {
const intersectionObserverRef = useRef<IntersectionObserver | null>(null)

return useCallback(
(node: HTMLDivElement | null) => {
if (intersectionObserverRef.current) {
intersectionObserverRef.current.disconnect()
}

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

if (node) intersectionObserverRef.current.observe(node)
},
[onIntersectCallback],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useCallback, useEffect, useRef, useState } from "react"
import { Link, useLocation } from "react-router"
import { useIntersectionCallback } from "../../app/useIntersectionCallback"
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 = useIntersectionCallback(fetchPreviousPage)
const afterRef = useIntersectionCallback(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} />
{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>
<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
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

0 comments on commit 78ff764

Please sign in to comment.