{"id":16368603,"url":"https://github.com/simoneb/token-pagination-hooks","last_synced_at":"2025-10-26T07:31:00.325Z","repository":{"id":57377713,"uuid":"325602994","full_name":"simoneb/token-pagination-hooks","owner":"simoneb","description":"React Hooks library to use classic pagination in a frontend with a token-based pagination backend.","archived":false,"fork":false,"pushed_at":"2022-02-27T12:46:08.000Z","size":89,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-30T03:55:39.807Z","etag":null,"topics":["hooks","pagination","react","token-pagination"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simoneb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-30T16:55:53.000Z","updated_at":"2022-11-30T12:38:10.000Z","dependencies_parsed_at":"2022-09-19T03:30:17.040Z","dependency_job_id":null,"html_url":"https://github.com/simoneb/token-pagination-hooks","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simoneb%2Ftoken-pagination-hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simoneb%2Ftoken-pagination-hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simoneb%2Ftoken-pagination-hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simoneb%2Ftoken-pagination-hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simoneb","download_url":"https://codeload.github.com/simoneb/token-pagination-hooks/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238284768,"owners_count":19446723,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hooks","pagination","react","token-pagination"],"created_at":"2024-10-11T02:53:15.340Z","updated_at":"2025-10-26T07:30:55.034Z","avatar_url":"https://github.com/simoneb.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003e The license of this software has changed to AWISC - Anti War ISC License\n\n# token-pagination-hooks\n\n![ci](https://github.com/simoneb/token-pagination-hooks/workflows/ci/badge.svg)\n[![codecov](https://codecov.io/gh/simoneb/token-pagination-hooks/branch/master/graph/badge.svg?token=QYDQYG7WN5)](https://codecov.io/gh/simoneb/token-pagination-hooks)\n[![npm version](https://badge.fury.io/js/token-pagination-hooks.svg)](https://badge.fury.io/js/token-pagination-hooks)\n[![bundlephobia](https://badgen.net/bundlephobia/minzip/token-pagination-hooks)](https://bundlephobia.com/result?p=token-pagination-hooks)\n[![bundlephobia](https://badgen.net/bundlephobia/dependency-count/token-pagination-hooks)](https://bundlephobia.com/result?p=token-pagination-hooks)\n\nReact Hooks library to use classic pagination in a frontend, based on page number and page size, with a token-based pagination backend.\n\n\u003c!-- toc --\u003e\n\n- [token-pagination-hooks](#token-pagination-hooks)\n  - [Setup](#setup)\n  - [Quickstart](#quickstart)\n    - [Backend](#backend)\n    - [Frontend](#frontend)\n  - [Running the examples](#running-the-examples)\n  - [API](#api)\n  - [Usage](#usage)\n    - [Token update](#token-update)\n      - [Declarative](#declarative)\n      - [Imperative](#imperative)\n    - [Modes](#modes)\n      - [Controlled](#controlled)\n      - [Uncontrolled](#uncontrolled)\n    - [Persistence](#persistence)\n      - [A working example](#a-working-example)\n\n\u003c!-- tocstop --\u003e\n\n## Setup\n\n```bash\nnpm i token-pagination-hooks\n```\n\n## Quickstart\n\nThe hook can work in `controlled` and `uncontrolled` modes, as is the React convention. See more details in the [usage](#usage) section. This example uses the controlled mode.\n\n### Backend\n\n[![Edit server](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/server-0wmht?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\nAssiming you're using an API which:\n\n- accepts a `pageToken` query string parameter to do pagination\n\n```bash\nGET /api?pageSize=2\u0026pageToken=some-opaque-string\n```\n\n- returns data in the format:\n\n```json\n{\n  \"data\": [{ \n    \"id\": 1, \n    \"value\": \"some value\" \n  }],\n  \"nextPage\": \"some-opaque-string\"\n}\n```\n\n### Frontend\n\n[![Edit with axios-hooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/with-axios-hooks-u035y?fontsize=14\u0026hidenavigation=1\u0026theme=dark)\n\nAssuming you're using a library like [axios-hooks](https://github.com/simoneb/axios-hooks/) to interact with the API:\n\n```jsx\nfunction Pagination() {\n  // store pagination state\n  const [pageNumber, setPageNumber] = useState(1)\n\n  // use the hook and provide the current page number\n  const { currentToken, useUpdateToken, hasToken } = useTokenPagination(\n    pageNumber\n  )\n\n  // invoke the paginated api\n  const [{ data }] = useAxios({\n    url: '/api',\n    params: { pageSize: 3, pageToken: currentToken }\n  })\n\n  // update the token for the next page\n  useUpdateToken(data?.nextPage)\n\n  return (\n    \u003c\u003e\n      \u003cpre\u003e{JSON.stringify(data, null, 2)}\u003c/pre\u003e\n      \u003cdiv\u003e\n        \u003cbutton\n          disabled={pageNumber \u003c= 1}\n          onClick={() =\u003e setPageNumber((p) =\u003e p - 1)}\n        \u003e\n          \u0026lt;\u0026lt;\n        \u003c/button\u003e\n        {'  '}\n        {pageNumber}\n        {'  '}\n        \u003cbutton\n          disabled={!hasToken(pageNumber + 1) || !data?.nextPage}\n          onClick={() =\u003e setPageNumber((p) =\u003e p + 1)}\n        \u003e\n          \u0026gt;\u0026gt;\n        \u003c/button\u003e\n      \u003c/div\u003e\n    \u003c/\u003e\n  )\n}\n```\n\n## Running the examples\n\nThe repository contains several examples showing different usage scenarios. To run the examples:\n\n- clone the repository and cd into it\n- `npm i`\n- `npm run examples`\n- browse to `http://localhost:4000`\n\n## API\n\n```jsx\nimport useTokenPagination from 'token-pagination-hooks'\n\nfunction Component() {\n  const result = useTokenPagination(options[, stateHookFactory])\n}\n```\n\n- `options` - `number` | `object` - **Required**\n  - `number`\n  \n    Represents a page number and implies the `controlled` mode. The page number must be provided and its value reflect the current page.\n\n  - `object` \n   \n    Implies the `uncontrolled` mode.\n\n    - `options.defaultPageNumber` - `number` - **Default: 1**\n          \n      The initial page number. The Hook will then keep its internal state.\n\n    - `options.defaultPageSize` - `number` - **Required**\n\n      The initial page size. The Hook will then keep its internal state.\n\n    - `options.resetPageNumberOnPageSizeChange` -`bool` - **Default: true**\n\n      Whether to reset the page number when the page size changes.\n\n- `stateHookFactory` - `(key: string) =\u003e function` - **Optional**\n\n  An optional factory for the state Hook which defaults to a function returning `React.useState`.\n\n  It can be customized to provide a Hook which stores the state in a persistent store, like browser storage.\n\n  It should be a function which accepts a unique key and returns a Hook implementation.\n\n- `result` - `object`\n  \n  The return value of the Hook, its properties change depending on whether `controlled` or `uncontrolled` mode is used.\n\n  **Both controlled and uncontrolled**\n\n  - `result.currentToken` - `any`\n   \n    The pagination token for the requested page to provide to the API.\n  \n  - `result.useUpdateToken` - `token: any =\u003e void` \n  \n    The Hook to invoke with the pagination token as returned by the API for declarative storage of the mapping between page numbers and tokens.\n\n  - `result.updateToken` - `token: any =\u003e void`\n\n    The function to invoke with the pagination token as returned by the API for imperative storage of the mapping between page numbers and tokens.\n\n  - `hasToken` - `pageNumber: number =\u003e bool`\n  \n    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).\n\n  **Uncontrolled only**\n\n  - `result.pageNumber` - `number`\n  \n    The current page number.\n\n  - `result.pageSize` - `number`\n  \n    The current page size.\n\n  - `result.changePageNumber(changer)`\n\n    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.\n\n    `changer`:\n\n    - `pageNumber: number`\n    - `(previousPageNumber: number) =\u003e newPageNumber: number`\n\n  - `result.changePageSize(changer)`\n\n    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.\n\n    `changer`:\n\n    - `pageNumber: number`\n    - `(previousPageSize: number) =\u003e newPageSize: number`\n  \n    \n## Usage\n\n### Token update\n\nThe 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.\n\n#### Declarative\n\nThe 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`](https://github.com/simoneb/axios-hooks/), [`graphql-hooks`](https://github.com/nearform/graphql-hooks) or one of the many other Hook-based libraries available.\n\n```jsx\nconst { useUpdateToken } = useTokenPagination(...)\n\n// invoke your API which returns the token for the next page, e.g.\nconst { data, nextPage } = useYourApi()\n\n// update the token for the next page using the Hook\nuseUpdateToken(nextPage)\n```\n\n#### Imperative\n\nThe imperative approach is useful when you invoke your API imperatively, for instance using `fetch` in a `useEffect` Hook:\n\n```jsx\nconst { currentToken, updateToken } = useTokenPagination(...)\n\nuseEffect(() =\u003e {\n  async function fetchData() {\n    const params = new URLSearchParams({ pageToken: currentToken })\n\n    const res = await fetch(`/api?${params.toString()}`)\n    const data = await res.json()\n\n    // update the token imperatively when the API responds\n    updateToken(data.nextPage)\n  }\n\n  fetchData()\n}, [currentToken, updateToken])\n```\n\n### Modes\n\nThe hook can be used in `controlled` and `uncontrolled` mode. \n\n#### Controlled\n\nWhen in controlled mode, you are responsible for keeping the pagination state (page number, page size) and providing the necessary data to the Hook.\n\nTo work in controlled mode, you provide a numeric page number as the first argument to the Hook:\n\n```js\n// you are responsible for storing the pagination state\nconst [pageNumber, setPageNumber] = useState(1)\n\n// you provide the current page number to the hook\nconst { useUpdateToken } = useTokenPagination(pageNumber)\n\n// invoke your API which returns the token for the next page, e.g.\nconst { data, nextPage } = useYourApi()\n\n// inform the hook of the token to take you from the current page to the next\nuseUpdateToken(nextPage)\n```\n\n#### Uncontrolled\n\nWhen in uncontrolled mode, the hook keeps its internal pagination state and provides way to read and modify it.\n\nTo work in uncontrolled mode, you provide an object containing a default page number and a default page size:\n\n```jsx\n// you provide default values and the hook keeps its internal state\nconst {\n  useUpdateToken,\n  pageNumber,\n  pageSize,\n} = useTokenPagination({ defaultPageNumber: 1, defaultPageSize: 5 })\n\n// invoke your API which returns the token for the next page, e.g.\nconst { data, nextPage } = useYourApi()\n\n// inform  the hook of the token to take you from the current page to the next\nuseUpdateToken(nextPage)\n```\n\n### Persistence\n\n\u003e An ideal complement to this library is [navigation-state-hooks](https://github.com/simoneb/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.\n\nBy default the pagination state is kept in the component state using React's `useState` Hook.\n\nThis can be customized providing a `stateHookFactory` as the second argument to the Hook.\n\n```jsx\nconst result = useTokenPagination(1, stateHookFactory)\n```\n\n`stateHookFactory` is a function which takes a unique key and returns a React Hook whose API is the same as React's `useState` Hook.\n\nFor example, if you wanted to persist data in the browser's `sessionStorage`, you could write a Hook like this:\n\n```jsx\nfunction makeStateHook(key) {\n  return function useSessionStorageState(initializer) {\n    const result = useState(\n      JSON.parse(sessionStorage.getItem(key) || 'null') ?? initializer\n    )\n\n    const [state] = result\n\n    useEffect(() =\u003e {\n      sessionStorage.setItem(key, JSON.stringify(state))\n    }, [state])\n\n    return result\n  }\n}\n```\n\nWhen invoking the above `makeStateHook` function with:\n\n```jsx\nconst useSessionStorageState = makeStateHook('some-key')\n```\n\nyou 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`.\n\nTo 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.\n\n#### A working example\n\nThe final implementation looks like:\n\n```jsx\nfunction makeStateHookFactory(prefix) {\n  return function makeStateHook(key) {\n    const id = [prefix, key].join('-')\n\n    return function useSessionStorageState(initializer) {\n      const result = useState(\n        JSON.parse(sessionStorage.getItem(id) || 'null') ?? initializer\n      )\n\n      const [state] = result\n\n      useEffect(() =\u003e {\n        sessionStorage.setItem(id, JSON.stringify(state))\n      }, [state])\n\n      return result\n    }\n  }\n}\n```\n\nand it can be used as:\n\n```jsx\nconst stateHookFactory = makeStateHookFactory('session-storage-key')\n\nfunction Component() {\n  const result = useTokenPagination(1, stateHookFactory)\n\n  ...\n}\n\n```\n\nA working example of persistence in action is in the [examples](examples) folder.\n\nAs long as this interface is respected you can use any Hooks as an alternative to the component local state.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoneb%2Ftoken-pagination-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimoneb%2Ftoken-pagination-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoneb%2Ftoken-pagination-hooks/lists"}