Skip to content

Commit d4a1880

Browse files
committed
feat: persistence
1 parent d286ac5 commit d4a1880

11 files changed

+197
-23
lines changed

examples/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"Source": true,
1313
"ControlledDeclarative": true,
1414
"ControlledImperative": true,
15-
"Uncontrolled": true
15+
"Uncontrolled": true,
16+
"Persistence": true
1617
}
1718
}

examples/components/Example.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const choices = [
22
ControlledDeclarative,
33
ControlledImperative,
44
Uncontrolled,
5+
Persistence,
56
].reduce((acc, c) => ({ ...acc, [c.name]: c }), {})
67

78
function Example() {

examples/components/Persistence.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { useState, useEffect } = React
2+
3+
function Persistence() {
4+
const {
5+
currentToken,
6+
hasToken,
7+
useUpdateToken,
8+
changePageNumber,
9+
changePageSize,
10+
pageNumber,
11+
pageSize,
12+
} = useTokenPagination({
13+
defaultPageNumber: 1,
14+
defaultPageSize: 5,
15+
persister: useTokenPagination.localPersister('persistence'),
16+
})
17+
const [data, setData] = useState()
18+
19+
useEffect(() => {
20+
async function fetchData() {
21+
const params = new URLSearchParams({ pageSize })
22+
23+
if (currentToken) {
24+
params.append('pageToken', currentToken)
25+
}
26+
27+
const res = await fetch(`/api?${params.toString()}`)
28+
const data = await res.json()
29+
30+
setData(data)
31+
}
32+
33+
fetchData()
34+
}, [pageSize, currentToken])
35+
36+
useUpdateToken(data?.nextPage)
37+
38+
function previousPage() {
39+
changePageNumber(n => n - 1)
40+
}
41+
function nextPage() {
42+
changePageNumber(n => n + 1)
43+
}
44+
function handleChangePageSize(e) {
45+
changePageSize(+e.target.value)
46+
}
47+
48+
return (
49+
<Output
50+
data={data}
51+
pageNumber={pageNumber}
52+
pageSize={pageSize}
53+
changePageSize={handleChangePageSize}
54+
previousPage={previousPage}
55+
nextPage={hasToken(pageNumber + 1) ? nextPage : undefined}
56+
/>
57+
)
58+
}
59+
60+
Persistence.propTypes = {}

examples/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
></script>
1717
<script type="text/babel" src="components/ControlledImperative.js"></script>
1818
<script type="text/babel" src="components/Uncontrolled.js"></script>
19+
<script type="text/babel" src="components/Persistence.js"></script>
1920
<script type="text/babel" src="components/Example.js"></script>
2021
</head>
2122
<body>

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module.exports = {
1414
// cacheDirectory: "C:\\Users\\simone\\AppData\\Local\\Temp\\jest",
1515

1616
// Automatically clear mock calls and instances between every test
17-
clearMocks: true,
17+
// clearMocks: true,
1818

1919
// Indicates whether the coverage information should be collected while executing the test
2020
// collectCoverage: false,

rollup.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default {
55
dir: 'cjs',
66
format: 'cjs',
77
exports: 'default',
8+
esModule: false,
89
},
910
{
1011
dir: 'es',

src/controlled.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import { useCallback, useEffect, useState, useMemo } from 'react'
2+
import { NULL_PERSISTER } from './persisters'
23
import { assertNumber } from './utils'
34

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

7-
const [mapping, setMapping] = useState({})
14+
const [mapping, setMapping] = useState(() => {
15+
const { mapping } = options.persister.hydrate()
16+
return mapping || {}
17+
})
818

919
const updateToken = useCallback(
1020
nextToken => {
@@ -23,6 +33,10 @@ export default function useControlledTokenPagination(pageNumber) {
2333
[updateToken]
2434
)
2535

36+
useEffect(() => {
37+
options.persister.persist({ mapping })
38+
}, [options.persister, mapping])
39+
2640
return useMemo(
2741
() => ({
2842
currentToken: mapping[pageNumber],

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import useControlledTokenPagination from './controlled'
22
import useUncontrolledTokenPagination from './uncontrolled'
3+
import * as persisters from './persisters'
34

45
const variants = {
56
number: useControlledTokenPagination,
@@ -15,3 +16,5 @@ export default function useTokenPagination(options) {
1516

1617
return variant(options)
1718
}
19+
20+
Object.assign(useTokenPagination, persisters)

src/persisters.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export const NULL_PERSISTER = {
2+
hydrate() {
3+
return {}
4+
},
5+
persist() {},
6+
}
7+
8+
function StoragePersister(key, storage) {
9+
return {
10+
hydrate() {
11+
try {
12+
return JSON.parse(storage.getItem(key) || '{}')
13+
} catch (err) {
14+
return {}
15+
}
16+
},
17+
persist(value) {
18+
storage.setItem(key, JSON.stringify({ ...this.hydrate(), ...value }))
19+
},
20+
}
21+
}
22+
23+
export const localPersister = key =>
24+
new StoragePersister(key, window.localStorage)
25+
export const sessionPersister = key =>
26+
new StoragePersister(key, window.sessionStorage)

src/persisters.test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { localPersister, sessionPersister } from './persisters'
2+
3+
describe('persisters', () => {
4+
describe('local', persisterTests(localPersister, 'localStorage'))
5+
6+
describe('session', persisterTests(sessionPersister, 'sessionStorage'))
7+
})
8+
9+
function persisterTests(persisterFactory, storageName) {
10+
return function () {
11+
let persister, storage
12+
13+
beforeEach(() => {
14+
Object.defineProperty(window, storageName, {
15+
value: { setItem: jest.fn(), getItem: jest.fn() },
16+
})
17+
storage = window[storageName]
18+
persister = persisterFactory('key')
19+
})
20+
21+
it('persists', () => {
22+
persister.persist({ some: 'value' })
23+
24+
expect(storage.setItem).toHaveBeenCalledWith(
25+
'key',
26+
JSON.stringify({ some: 'value' })
27+
)
28+
})
29+
30+
it('hydrates', () => {
31+
const stored = { some: 'value' }
32+
storage.getItem.mockReturnValue(JSON.stringify(stored))
33+
34+
expect(persister.hydrate()).toEqual(stored)
35+
})
36+
37+
it('merges with existing value when persisting', () => {
38+
const stored = { some: 'value' }
39+
storage.getItem.mockReturnValue(JSON.stringify(stored))
40+
41+
persister.persist({ another: 'value' })
42+
43+
expect(storage.setItem).toHaveBeenCalledWith(
44+
'key',
45+
JSON.stringify({ some: 'value', another: 'value' })
46+
)
47+
})
48+
49+
it('handles errors', () => {
50+
storage.getItem.mockReturnValue('invalid json')
51+
expect(persister.hydrate()).toEqual({})
52+
})
53+
}
54+
}

src/uncontrolled.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,67 @@
1-
import { useCallback, useState, useMemo } from 'react'
1+
import { useCallback, useState, useMemo, useEffect } from 'react'
22
import useControlledTokenPagination from './controlled'
3+
import { NULL_PERSISTER } from './persisters'
34
import { assertNumber } from './utils'
45

56
const DEFAULTS = {
67
defaultPageNumber: 1,
78
resetPageNumberOnPageSizeChange: true,
9+
persister: NULL_PERSISTER,
810
}
911

12+
const changerTypes = ['function', 'number']
13+
1014
export default function useUncontrolledTokenPagination(options) {
1115
options = { ...DEFAULTS, ...options }
1216

1317
assertNumber('defaultPageNumber', options.defaultPageNumber)
1418
assertNumber('defaultPageSize', options.defaultPageSize)
1519

16-
const [{ pageNumber, pageSize }, setPagination] = useState({
17-
pageNumber: options.defaultPageNumber,
18-
pageSize: options.defaultPageSize,
20+
const [{ pageNumber, pageSize }, setPagination] = useState(() => {
21+
const { pageNumber, pageSize } = options.persister.hydrate()
22+
23+
return {
24+
pageNumber: pageNumber || options.defaultPageNumber,
25+
pageSize: pageSize || options.defaultPageSize,
26+
}
1927
})
2028

2129
const change = useCallback(
2230
(property, changer) => {
2331
const pageNumberReset = options.resetPageNumberOnPageSizeChange
2432
? { pageNumber: options.defaultPageNumber }
25-
: {}
33+
: null
2634

2735
const changerType = typeof changer
2836

29-
if (!['function', 'number'].includes(changerType)) {
37+
if (!changerTypes.includes(changerType)) {
3038
throw new Error(
31-
`Unsupported value ${changer} of type ${changerType} for ${property}`
39+
`Unsupported value ${changer} of type ${changerType} for ${property}. Supported values are ${changerTypes}`
3240
)
3341
}
3442

35-
const paginate = p => {
36-
return {
37-
...p,
38-
...pageNumberReset,
39-
[property]: changerType === 'number' ? changer : changer(p[property]),
40-
}
41-
}
42-
43-
setPagination(paginate)
43+
setPagination(p => ({
44+
...p,
45+
...pageNumberReset,
46+
[property]: changerType === 'function' ? changer(p[property]) : changer,
47+
}))
4448
},
4549
[options.defaultPageNumber, options.resetPageNumberOnPageSizeChange]
4650
)
4751

48-
const changePageNumber = useCallback(c => change('pageNumber', c), [change])
49-
const changePageSize = useCallback(c => change('pageSize', c), [change])
52+
const changePageNumber = useCallback(
53+
changer => change('pageNumber', changer),
54+
[change]
55+
)
56+
const changePageSize = useCallback(changer => change('pageSize', changer), [
57+
change,
58+
])
59+
60+
const controlled = useControlledTokenPagination(pageNumber, options)
5061

51-
const controlled = useControlledTokenPagination(pageNumber)
62+
useEffect(() => {
63+
options.persister.persist({ pageNumber, pageSize })
64+
}, [options.persister, pageNumber, pageSize])
5265

5366
return useMemo(
5467
() => ({

0 commit comments

Comments
 (0)