token-pagination-hooks
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
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
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)}
>
<<
</button>
{' '}
{pageNumber}
{' '}
<button
disabled={!hasToken(pageNumber + 1) || !data?.nextPage}
onClick={() => setPageNumber((p) => p + 1)}
>
>>
</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: 1The initial page number. The Hook will then keep its internal state.
-
options.defaultPageSize
-number
- RequiredThe initial page size. The Hook will then keep its internal state.
-
options.resetPageNumberOnPageSizeChange
-bool
- Default: trueWhether to reset the page number when the page size changes.
-
-
-
stateHookFactory
-(key: string) => function
- OptionalAn 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
oruncontrolled
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 Reac 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.