Skip to content

Commit b405ea3

Browse files
committedJan 1, 2021
feat: docs and API finalization
1 parent 681244a commit b405ea3

File tree

7 files changed

+246
-39
lines changed

7 files changed

+246
-39
lines changed
 

‎README.md

+219-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![bundlephobia](https://badgen.net/bundlephobia/minzip/token-pagination-hooks)](https://bundlephobia.com/result?p=token-pagination-hooks)
77
[![bundlephobia](https://badgen.net/bundlephobia/dependency-count/token-pagination-hooks)](https://bundlephobia.com/result?p=token-pagination-hooks)
88

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

1111
<!-- toc -->
1212

@@ -19,19 +19,26 @@ React Hooks library to use classic pagination in the frontend with a token-based
1919

2020
## Setup
2121

22-
`npm i token-pagination-hooks`
22+
```bash
23+
npm i token-pagination-hooks
24+
```
2325

2426
## Quickstart
2527

26-
### API
28+
The 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.
2729

28-
See example API:
30+
### Backend
2931

3032
[![Edit server](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/server-0wmht?fontsize=14&hidenavigation=1&theme=dark)
3133

3234
Assiming you're using an API which:
3335

3436
- accepts a `pageToken` query string parameter to do pagination
37+
38+
```bash
39+
GET /api?pageSize=2&pageToken=some-opaque-string
40+
```
41+
3542
- returns data in the format:
3643

3744
```json
@@ -40,33 +47,33 @@ Assiming you're using an API which:
4047
"id": 1,
4148
"value": "some value"
4249
}],
43-
"nextPage": "some opaque string"
50+
"nextPage": "some-opaque-string"
4451
}
4552
```
4653

47-
### Client
48-
49-
See example client:
54+
### Frontend
5055

5156
[![Edit with axios-hooks](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/with-axios-hooks-u035y?fontsize=14&hidenavigation=1&theme=dark)
5257

5358
Assuming you're using a library like [axios-hooks](https://github.com/simoneb/axios-hooks/) to interact with the API:
5459

55-
```js
56-
import React, { useState } from 'react'
57-
import useAxios from 'axios-hooks'
58-
import useTokenPagination from 'token-pagination-hooks'
59-
60+
```jsx
6061
function Pagination() {
62+
// store pagination state
6163
const [pageNumber, setPageNumber] = useState(1)
64+
65+
// use the hook and provide the current page number
6266
const { currentToken, useUpdateToken, hasToken } = useTokenPagination(
6367
pageNumber
6468
)
69+
70+
// invoke the paginated api
6571
const [{ data }] = useAxios({
6672
url: '/api',
6773
params: { pageSize: 3, pageToken: currentToken }
6874
})
6975

76+
// update the token for the next page
7077
useUpdateToken(data?.nextPage)
7178

7279
return (
@@ -93,3 +100,202 @@ function Pagination() {
93100
)
94101
}
95102
```
103+
104+
## Running the examples
105+
106+
The repository contains several examples showing different usage scenarios. To run the examples:
107+
108+
- clone the repository and cd into it
109+
- `npm i`
110+
- `npm run examples`
111+
- browse to `http://localhost:4000`
112+
113+
## API
114+
115+
```jsx
116+
import useTokenPagination, {
117+
localPersister,
118+
sessionPersister
119+
} from 'token-pagination-hooks'
120+
121+
const persister = localPersister('key') // OR sessionPersister('key')
122+
123+
function Component() {
124+
const result = useTokenPagination(options, persister?)
125+
}
126+
```
127+
128+
- `options` - `number` | `object` - **Required**
129+
- `number`
130+
131+
Represents a page number and implies the `controlled` mode. The page number must be provided and its value reflect the current page.
132+
133+
- `object`
134+
135+
Implies the `uncontrolled` mode.
136+
137+
- `options.defaultPageNumber` - `number` - **Default: 1**
138+
139+
The initial page number. The Hook will then keep its internal state.
140+
141+
- `options.defaultPageSize` - `number` - **Required**
142+
143+
The initial page size. The Hook will then keep its internal state.
144+
145+
- `options.resetPageNumberOnPageSizeChange` -`bool` - **Default: true**
146+
147+
Whether to reset the page number when the page size changes.
148+
149+
- `persister` - `object` - **Optional**
150+
151+
An optional persister to store the pagination state for later retrieval. Some persisters are provided with the library and others can be implemented by providing an object (or an instance of a class) adhering to the following interface:
152+
153+
- `persister.hydrate` - `() => object`
154+
155+
Method that is called to read the pagination data from the persistent store.
156+
157+
- `persister.persist` - `object => void`
158+
159+
Method that is called with an object representing the pagination state and which should persist it to the persistent store for later retrieval.
160+
161+
- `result` - `object`
162+
163+
The return value of the Hook, its properties change depending on whether `controlled` or `uncontrolled` mode is used.
164+
165+
**Both controlled and uncontrolled**
166+
167+
- `result.currentToken` - `any`
168+
169+
The pagination token for the requested page to provide to the API.
170+
171+
- `result.useUpdateToken` - `token: any => void`
172+
173+
The Hook to invoke with the pagination token as returned by the API for declarative storage of the mapping between page numbers and tokens.
174+
175+
- `result.updateToken` - `token: any => void`
176+
177+
The function to invoke with the pagination token as returned by the API for imperative storage of the mapping between page numbers and tokens.
178+
179+
- `hasToken` - `pageNumber: number => bool`
180+
181+
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).
182+
183+
**Uncontrolled only**
184+
185+
- `result.pageNumber` - `number`
186+
187+
The current page number.
188+
189+
- `result.pageSize` - `number`
190+
191+
The current page size.
192+
193+
- `result.changePageNumber(changer)`
194+
195+
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.
196+
197+
`changer`:
198+
199+
- `pageNumber: number`
200+
- `(previousPageNumber: number) => newPageNumber: number`
201+
202+
- `result.changePageSize(changer)`
203+
204+
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.
205+
206+
`changer`:
207+
208+
- `pageNumber: number`
209+
- `(previousPageSize: number) => newPageSize: number`
210+
211+
212+
## Usage
213+
214+
### Token update
215+
216+
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.
217+
218+
#### Declarative
219+
220+
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`](https://github.com/simoneb/axios-hooks/), [`graphql-hooks`](https://github.com/nearform/graphql-hooks) or one of the many other Hook-based libraries available.
221+
222+
```jsx
223+
const { useUpdateToken } = useTokenPagination(...)
224+
225+
// invoke your API which returns the token for the next page, e.g.
226+
const { data, nextPage } = useYourApi()
227+
228+
// update the token for the next page using the Hook
229+
useUpdateToken(nextPage)
230+
```
231+
232+
#### Imperative
233+
234+
The imperative approach is useful when you invoke your API imperatively, for instance using `fetch` in a `useEffect` Hook:
235+
236+
```jsx
237+
const { currentToken, updateToken } = useTokenPagination(...)
238+
239+
useEffect(() => {
240+
async function fetchData() {
241+
const params = new URLSearchParams({ pageToken: currentToken })
242+
243+
const res = await fetch(`/api?${params.toString()}`)
244+
const data = await res.json()
245+
246+
// update the token imperatively when the API responds
247+
updateToken(data.nextPage)
248+
}
249+
250+
fetchData()
251+
}, [currentToken, updateToken])
252+
```
253+
254+
### Modes
255+
256+
The hook can be used in `controlled` and `uncontrolled` mode.
257+
258+
#### Controlled
259+
260+
When in controlled mode, you are responsible for keeping the pagination state (page number, page size) and providing the necessary data to the Hook.
261+
262+
To work in controlled mode, you provide a numeric page number as the first argument to the Hook:
263+
264+
```js
265+
// you are responsible for storing the pagination state
266+
const [pageNumber, setPageNumber] = useState(1)
267+
268+
// you provide the current page number to the hook
269+
const { useUpdateToken } = useTokenPagination(pageNumber)
270+
271+
// invoke your API which returns the token for the next page, e.g.
272+
const { data, nextPage } = useYourApi()
273+
274+
// inform the hook of the token to take you from the current page to the next
275+
useUpdateToken(nextPage)
276+
```
277+
278+
#### Uncontrolled
279+
280+
When in uncontrolled mode, the hook keeps its internal pagination state and provides way to read and modify it.
281+
282+
To work in uncontrolled mode, you provide an object containing a default page number and a default page size:
283+
284+
```jsx
285+
// you provide default values and the hook keeps its internal state
286+
const {
287+
useUpdateToken,
288+
pageNumber,
289+
pageSize,
290+
} = useTokenPagination({ defaultPageNumber: 1, defaultPageSize: 5 })
291+
292+
293+
// invoke your API which returns the token for the next page, e.g.
294+
const { data, nextPage } = useYourApi()
295+
296+
// inform the hook of the token to take you from the current page to the next
297+
useUpdateToken(nextPage)
298+
```
299+
300+
301+

‎examples/components/Persistence.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ function Persistence() {
99
changePageSize,
1010
pageNumber,
1111
pageSize,
12-
} = useTokenPagination({
13-
defaultPageNumber: 1,
14-
defaultPageSize: 5,
15-
persister: useTokenPagination.localPersister('persistence'),
16-
})
12+
} = useTokenPagination(
13+
{
14+
defaultPageNumber: 1,
15+
defaultPageSize: 5,
16+
},
17+
useTokenPagination.sessionPersister('persistence')
18+
)
1719
const [data, setData] = useState()
1820

1921
useEffect(() => {

‎examples/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html>
33
<head>
44
<meta charset="utf-8" />
5-
<title>Example</title>
5+
<title>Examples - token-pagination-hooks</title>
66
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
77
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
88
<script src="https://unpkg.com/prop-types@15/prop-types.js"></script>

‎src/controlled.js

+7-10
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@ import { useCallback, useEffect, useState, useMemo } from 'react'
22
import { NULL_PERSISTER } from './persisters'
33
import { assertNumber } from './utils'
44

5-
const DEFAULTS = {
6-
persister: NULL_PERSISTER,
7-
}
8-
9-
export default function useControlledTokenPagination(pageNumber, options) {
10-
options = { ...DEFAULTS, ...options }
11-
5+
export default function useControlledTokenPagination(
6+
pageNumber,
7+
persister = NULL_PERSISTER
8+
) {
129
assertNumber('pageNumber', pageNumber)
1310

1411
const [mapping, setMapping] = useState(() => {
15-
const { mapping } = options.persister.hydrate()
12+
const { mapping } = persister.hydrate()
1613
return mapping || {}
1714
})
1815

@@ -34,8 +31,8 @@ export default function useControlledTokenPagination(pageNumber, options) {
3431
)
3532

3633
useEffect(() => {
37-
options.persister.persist({ mapping })
38-
}, [options.persister, mapping])
34+
persister.persist({ mapping })
35+
}, [persister, mapping])
3936

4037
return useMemo(
4138
() => ({

‎src/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ const variants = {
77
object: useUncontrolledTokenPagination,
88
}
99

10-
export default function useTokenPagination(options) {
10+
export default function useTokenPagination(options, persister) {
1111
const variant = variants[typeof options]
1212

1313
if (!variant) {
1414
throw new Error(`Unsupported options ${options} of type ${typeof options}`)
1515
}
1616

17-
return variant(options)
17+
return variant(options, persister)
1818
}
1919

2020
Object.assign(useTokenPagination, persisters)

‎src/index.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ describe('useTokenPagination', () => {
1212
const { result } = renderHook(() => useTokenPagination(1))
1313

1414
expect(result.current).toBe('controlled')
15-
expect(controlled).toHaveBeenCalledWith(1)
15+
expect(controlled).toHaveBeenCalledWith(1, undefined)
1616
})
1717

1818
it('returns uncontrolled when an object is provided', async () => {
1919
const arg = {}
2020
const { result } = renderHook(() => useTokenPagination(arg))
2121

2222
expect(result.current).toBe('uncontrolled')
23-
expect(uncontrolled).toHaveBeenCalledWith(arg)
23+
expect(uncontrolled).toHaveBeenCalledWith(arg, undefined)
2424
})
2525

2626
it('throws when an unknown input type is provided', async () => {

‎src/uncontrolled.js

+8-6
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,21 @@ import { assertNumber } from './utils'
66
const DEFAULTS = {
77
defaultPageNumber: 1,
88
resetPageNumberOnPageSizeChange: true,
9-
persister: NULL_PERSISTER,
109
}
1110

1211
const changerTypes = ['function', 'number']
1312

14-
export default function useUncontrolledTokenPagination(options) {
13+
export default function useUncontrolledTokenPagination(
14+
options,
15+
persister = NULL_PERSISTER
16+
) {
1517
options = { ...DEFAULTS, ...options }
1618

1719
assertNumber('defaultPageNumber', options.defaultPageNumber)
1820
assertNumber('defaultPageSize', options.defaultPageSize)
1921

2022
const [{ pageNumber, pageSize }, setPagination] = useState(() => {
21-
const { pageNumber, pageSize } = options.persister.hydrate()
23+
const { pageNumber, pageSize } = persister.hydrate()
2224

2325
return {
2426
pageNumber: pageNumber || options.defaultPageNumber,
@@ -57,11 +59,11 @@ export default function useUncontrolledTokenPagination(options) {
5759
change,
5860
])
5961

60-
const controlled = useControlledTokenPagination(pageNumber, options)
62+
const controlled = useControlledTokenPagination(pageNumber, persister)
6163

6264
useEffect(() => {
63-
options.persister.persist({ pageNumber, pageSize })
64-
}, [options.persister, pageNumber, pageSize])
65+
persister.persist({ pageNumber, pageSize })
66+
}, [persister, pageNumber, pageSize])
6567

6668
return useMemo(
6769
() => ({

0 commit comments

Comments
 (0)
Please sign in to comment.