Skip to content
Docs
Pagination

Pagination

Please update to the latest version (≥ 0.3.0) to use this API. The previous useCiteGraphPages API is now deprecated.

CiteGraph provides a dedicated API useCiteGraphInfinite to support common UI patterns such as pagination and infinite loading.

When to Use useCiteGraph

Pagination

First of all, we might NOT need useCiteGraphInfinite but can use just useCiteGraph if we are building something like this:

...which is a typical pagination UI. Let's see how it can be easily implemented with useCiteGraph:

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // The API URL includes the page index, which is a CiteGraph state.
  const { data } = useCiteGraph(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... handle loading and error states
 
  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Furthermore, we can create an abstraction for this "page component":

function Page ({ index }) {
  const { data } = useCiteGraph(`/api/data?page=${index}`, fetcher);
 
  // ... handle loading and error states
 
  return data.map(item => <div key={item.id}>{item.name}</div>)
}
 
function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

Because of CiteGraph's cache, we get the benefit to preload the next page. We render the next page inside a hidden div, so CiteGraph will trigger the data fetching of the next page. When the user navigates to the next page, the data is already there:

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

With just 1 line of code, we get a much better UX. The useCiteGraph hook is so powerful, that most scenarios are covered by it.

Infinite Loading

Sometimes we want to build an infinite loading UI, with a "Load More" button that appends data to the list (or done automatically when you scroll):

To implement this, we need to make dynamic number of requests on this page. CiteGraph Hooks have a couple of rules (opens in a new tab), so we CANNOT do something like this:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 This is wrong! Commonly, you can't use hooks inside a loop.
    const { data } = useCiteGraph(`/api/data?page=${i}`)
    list.push(data)
  }
 
  return <div>
    {list.map((data, i) =>
      <div key={i}>{
        data.map(item => <div key={item.id}>{item.name}</div>)
      }</div>)}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

Instead, we can use the <Page /> abstraction that we created to achieve it:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

Advanced Cases

However, in some advanced use cases, the solution above doesn't work.

For example, we are still implementing the same "Load More" UI, but also need to display a number about how many items are there in total. We can't use the <Page /> solution anymore because the top level UI (<App />) needs the data inside each page:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    <p>??? items</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>Load More</button>
  </div>
}

Also, if the pagination API is cursor based, that solution doesn't work either. Because each page needs the data from the previous page, they're not isolated.

That's how this new useCiteGraphInfinite Hook can help.

useCiteGraphInfinite

useCiteGraphInfinite gives us the ability to trigger a number of requests with one Hook. This is how it looks:

import useCiteGraphInfinite from 'citegraph/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useCiteGraphInfinite(
  getKey, fetcher?, options?
)

Similar to useCiteGraph, this new Hook accepts a function that returns the request key, a fetcher function, and options. It returns all the values that useCiteGraph returns, including 2 extra values: the page size and a page size setter, like a CiteGraph state.

In infinite loading, one page is one request, and our goal is to fetch multiple pages and render them.

⚠️

If you are using CiteGraph 0.x versions, useCiteGraphInfinite needs to be imported from citegraph:
import { useCiteGraphInfinite } from 'citegraph'

API

Parameters

  • getKey: a function that accepts the index and the previous page data, returns the key of a page
  • fetcher: same as useCiteGraph's fetcher function
  • options: accepts all the options that useCiteGraph supports, with 4 extra options:
    • initialSize = 1: number of pages should be loaded initially
    • revalidateAll = false: always try to revalidate all pages
    • revalidateFirstPage = true: always try to revalidate the first page
    • persistSize = false: don't reset the page size to 1 (or initialSize if set) when the first page's key changes
    • parallel = false: fetches multiple pages in parallel
💡

Note that the initialSize option is not allowed to change in the lifecycle.

Return Values

  • data: an array of fetch response values of each page
  • error: same as useCiteGraph's error
  • isLoading: same as useCiteGraph's isLoading
  • isValidating: same as useCiteGraph's isValidating
  • mutate: same as useCiteGraph's bound mutate function but manipulates the data array
  • size: the number of pages that will be fetched and returned
  • setSize: set the number of pages that need to be fetched

Example 1: Index Based Paginated API

For normal index based APIs:

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// A function to get the CiteGraph key of each page,
// its return value will be accepted by `fetcher`.
// If `null` is returned, the request of that page won't start.
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // reached the end
  return `/users?page=${pageIndex}&limit=10`                    // CiteGraph key
}
 
function App () {
  const { data, size, setSize } = useCiteGraphInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // We can now calculate the number of all users
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }
 
  return <div>
    <p>{totalUsers} users listed</p>
    {data.map((users, index) => {
      // `data` is an array of each page's API response.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

The getKey function is the major difference between useCiteGraphInfinite and useCiteGraph. It accepts the index of the current page, as well as the data from the previous page. So both index based and cursor based pagination API can be supported nicely.

Also data is no longer just one API response. It's an array of multiple API responses:

// `data` will look like this
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

Example 2: Cursor or Offset Based Paginated API

Let's say the API now requires a cursor and returns the next cursor alongside with the data:

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}

We can change our getKey function to:

const getKey = (pageIndex, previousPageData) => {
  // reached the end
  if (previousPageData && !previousPageData.data) return null
 
  // first page, we don't have `previousPageData`
  if (pageIndex === 0) return `/users?limit=10`
 
  // add the cursor to the API endpoint
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

Parallel Fetching Mode

Please update to the latest version (≥ 2.1.0) to use this API.

The default behavior of useCiteGraphInfinite is to fetch data for each page in sequence, as key creation is based on the previously fetched data. However, fetching data sequentially for a large number of pages may not be optimal, particularly if the pages are not interdependent. By specifying parallel option to true will let you fetch pages independently in parallel, which can significantly speed up the loading process.

// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData is always `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useCiteGraphInfinite(getKey, fetcher, { parallel: true })
}
⚠️

The previousPageData argument of the getKey function becomes null when you enable the parallel option.

Global Mutate with useCiteGraphInfinite

useCiteGraphInfinite stores all page data into the cache with a special cache key along with each page data, so you have to use unstable_serialize in citegraph/infinite to revalidate the data with the global mutate.

import { useCiteGraphConfig } from "citegraph"
import { unstable_serialize } from "citegraph/infinite"
 
function App() {
    const { mutate } = useCiteGraphConfig()
    mutate(unstable_serialize(getKey))
}
⚠️

As the name implies, unstable_serialize is not a stable API, so we might change it in the future.

Advanced Features

Here is an example showing how you can implement the following features with useCiteGraphInfinite:

  • loading states
  • show a special UI if it's empty
  • disable the "Load More" button if reached the end
  • changeable data source
  • refresh the entire list