Skip to content

React Hooks library to use classic pagination in a frontend with a token-based pagination backend.

License

Notifications You must be signed in to change notification settings

simoneb/token-pagination-hooks

Repository files navigation

The license of this software has changed to AWISC - Anti War ISC License

token-pagination-hooks

ci codecov npm version bundlephobia bundlephobia

React Hooks library to use classic pagination in a frontend, based on page number and page size, with a token-based pagination backend.

Setup

npm i token-pagination-hooks

Quickstart

The hook can work in controlled and uncontrolled modes, as is the React convention. See more details in the usage section. This example uses the controlled mode.

Backend

Edit server

Assiming you're using an API which:

  • accepts a pageToken query string parameter to do pagination
GET /api?pageSize=2&pageToken=some-opaque-string
  • returns data in the format:
{
  "data": [{ 
    "id": 1, 
    "value": "some value" 
  }],
  "nextPage": "some-opaque-string"
}

Frontend

Edit with axios-hooks

Assuming you're using a library like axios-hooks to interact with the API:

function Pagination() {
  // store pagination state
  const [pageNumber, setPageNumber] = useState(1)

  // use the hook and provide the current page number
  const { currentToken, useUpdateToken, hasToken } = useTokenPagination(
    pageNumber
  )

  // invoke the paginated api
  const [{ data }] = useAxios({
    url: '/api',
    params: { pageSize: 3, pageToken: currentToken }
  })

  // update the token for the next page
  useUpdateToken(data?.nextPage)

  return (
    <>
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <div>
        <button
          disabled={pageNumber <= 1}
          onClick={() => setPageNumber((p) => p - 1)}
        >
          &lt;&lt;
        </button>
        {'  '}
        {pageNumber}
        {'  '}
        <button
          disabled={!hasToken(pageNumber + 1) || !data?.nextPage}
          onClick={() => setPageNumber((p) => p + 1)}
        >
          &gt;&gt;
        </button>
      </div>
    </>
  )
}

Running the examples

The repository contains several examples showing different usage scenarios. To run the examples:

  • clone the repository and cd into it
  • npm i
  • npm run examples
  • browse to http://localhost:4000

API

import useTokenPagination from 'token-pagination-hooks'

function Component() {
  const result = useTokenPagination(options[, stateHookFactory])
}
  • options - number | object - Required

    • number

      Represents a page number and implies the controlled mode. The page number must be provided and its value reflect the current page.

    • object

      Implies the uncontrolled mode.

      • options.defaultPageNumber - number - Default: 1

        The initial page number. The Hook will then keep its internal state.

      • options.defaultPageSize - number - Required

        The initial page size. The Hook will then keep its internal state.

      • options.resetPageNumberOnPageSizeChange -bool - Default: true

        Whether to reset the page number when the page size changes.

  • stateHookFactory - (key: string) => function - Optional

    An optional factory for the state Hook which defaults to a function returning React.useState.

    It can be customized to provide a Hook which stores the state in a persistent store, like browser storage.

    It should be a function which accepts a unique key and returns a Hook implementation.

  • result - object

    The return value of the Hook, its properties change depending on whether controlled or uncontrolled mode is used.

    Both controlled and uncontrolled

    • result.currentToken - any

      The pagination token for the requested page to provide to the API.

    • result.useUpdateToken - token: any => void

      The Hook to invoke with the pagination token as returned by the API for declarative storage of the mapping between page numbers and tokens.

    • result.updateToken - token: any => void

      The function to invoke with the pagination token as returned by the API for imperative storage of the mapping between page numbers and tokens.

    • hasToken - pageNumber: number => bool

      A function which can be invoked with a page number to check if there is a pagination token for that page. Useful to conditionally enable pagination buttons (see examples).

    Uncontrolled only

    • result.pageNumber - number

      The current page number.

    • result.pageSize - number

      The current page size.

    • result.changePageNumber(changer)

      A function to change the page number. Changer is either a number, which will be the new page number, or a function, which gets the current page number as its first argument and returns the new page number.

      changer:

      • pageNumber: number
      • (previousPageNumber: number) => newPageNumber: number
    • result.changePageSize(changer)

      A function to change the page size. Changer is either a number, which will be the new page size, or a function, which gets the current page size as its first argument and returns the new page size.

      changer:

      • pageNumber: number
      • (previousPageSize: number) => newPageSize: number

Usage

Token update

The Hook provides two ways to update the mapping between a page number and the token used to paginate from the current page to the next: a declarative one based on a React Hook and an imperative one based on a plain function.

Declarative

The declarative approach is based on React Hooks and it's useful when you're invoking an API via a React Hook, as when using axios-hooks, graphql-hooks or one of the many other Hook-based libraries available.

const { useUpdateToken } = useTokenPagination(...)

// invoke your API which returns the token for the next page, e.g.
const { data, nextPage } = useYourApi()

// update the token for the next page using the Hook
useUpdateToken(nextPage)

Imperative

The imperative approach is useful when you invoke your API imperatively, for instance using fetch in a useEffect Hook:

const { currentToken, updateToken } = useTokenPagination(...)

useEffect(() => {
  async function fetchData() {
    const params = new URLSearchParams({ pageToken: currentToken })

    const res = await fetch(`/api?${params.toString()}`)
    const data = await res.json()

    // update the token imperatively when the API responds
    updateToken(data.nextPage)
  }

  fetchData()
}, [currentToken, updateToken])

Modes

The hook can be used in controlled and uncontrolled mode.

Controlled

When in controlled mode, you are responsible for keeping the pagination state (page number, page size) and providing the necessary data to the Hook.

To work in controlled mode, you provide a numeric page number as the first argument to the Hook:

// you are responsible for storing the pagination state
const [pageNumber, setPageNumber] = useState(1)

// you provide the current page number to the hook
const { useUpdateToken } = useTokenPagination(pageNumber)

// invoke your API which returns the token for the next page, e.g.
const { data, nextPage } = useYourApi()

// inform the hook of the token to take you from the current page to the next
useUpdateToken(nextPage)

Uncontrolled

When in uncontrolled mode, the hook keeps its internal pagination state and provides way to read and modify it.

To work in uncontrolled mode, you provide an object containing a default page number and a default page size:

// you provide default values and the hook keeps its internal state
const {
  useUpdateToken,
  pageNumber,
  pageSize,
} = useTokenPagination({ defaultPageNumber: 1, defaultPageSize: 5 })

// invoke your API which returns the token for the next page, e.g.
const { data, nextPage } = useYourApi()

// inform  the hook of the token to take you from the current page to the next
useUpdateToken(nextPage)

Persistence

An ideal complement to this library is navigation-state-hooks, which allows storing navigation state. In this way you can store pagination state per route, so when you navigate back to that route you can show the user the same page he was previously viewing.

By default the pagination state is kept in the component state using React's useState Hook.

This can be customized providing a stateHookFactory as the second argument to the Hook.

const result = useTokenPagination(1, stateHookFactory)

stateHookFactory is a function which takes a unique key and returns a React Hook whose API is the same as React's useState Hook.

For example, if you wanted to persist data in the browser's sessionStorage, you could write a Hook like this:

function makeStateHook(key) {
  return function useSessionStorageState(initializer) {
    const result = useState(
      JSON.parse(sessionStorage.getItem(key) || 'null') ?? initializer
    )

    const [state] = result

    useEffect(() => {
      sessionStorage.setItem(key, JSON.stringify(state))
    }, [state])

    return result
  }
}

When invoking the above makeStateHook function with:

const useSessionStorageState = makeStateHook('some-key')

you obtain a Hook which has the same interface as React's useState Hook and which loads the initial state and persist any subsequent state changes to the browser's sessionStorage with the key some-key.

To use such a Hook in this library you need to take one step further because the library needs to be able to store multiple keys, so it needs a prefix and a key.

A working example

The final implementation looks like:

function makeStateHookFactory(prefix) {
  return function makeStateHook(key) {
    const id = [prefix, key].join('-')

    return function useSessionStorageState(initializer) {
      const result = useState(
        JSON.parse(sessionStorage.getItem(id) || 'null') ?? initializer
      )

      const [state] = result

      useEffect(() => {
        sessionStorage.setItem(id, JSON.stringify(state))
      }, [state])

      return result
    }
  }
}

and it can be used as:

const stateHookFactory = makeStateHookFactory('session-storage-key')

function Component() {
  const result = useTokenPagination(1, stateHookFactory)

  ...
}

A working example of persistence in action is in the examples folder.

As long as this interface is respected you can use any Hooks as an alternative to the component local state.

About

React Hooks library to use classic pagination in a frontend with a token-based pagination backend.

Topics

Resources

License

Stars

Watchers

Forks