Skip to content

Commit

Permalink
feat: persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
simoneb committed Dec 31, 2020
1 parent d286ac5 commit d4a1880
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 23 deletions.
3 changes: 2 additions & 1 deletion examples/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"Source": true,
"ControlledDeclarative": true,
"ControlledImperative": true,
"Uncontrolled": true
"Uncontrolled": true,
"Persistence": true
}
}
1 change: 1 addition & 0 deletions examples/components/Example.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const choices = [
ControlledDeclarative,
ControlledImperative,
Uncontrolled,
Persistence,
].reduce((acc, c) => ({ ...acc, [c.name]: c }), {})

function Example() {
Expand Down
60 changes: 60 additions & 0 deletions examples/components/Persistence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const { useState, useEffect } = React

function Persistence() {
const {
currentToken,
hasToken,
useUpdateToken,
changePageNumber,
changePageSize,
pageNumber,
pageSize,
} = useTokenPagination({
defaultPageNumber: 1,
defaultPageSize: 5,
persister: useTokenPagination.localPersister('persistence'),
})
const [data, setData] = useState()

useEffect(() => {
async function fetchData() {
const params = new URLSearchParams({ pageSize })

if (currentToken) {
params.append('pageToken', currentToken)
}

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

setData(data)
}

fetchData()
}, [pageSize, currentToken])

useUpdateToken(data?.nextPage)

function previousPage() {
changePageNumber(n => n - 1)
}
function nextPage() {
changePageNumber(n => n + 1)
}
function handleChangePageSize(e) {
changePageSize(+e.target.value)
}

return (
<Output
data={data}
pageNumber={pageNumber}
pageSize={pageSize}
changePageSize={handleChangePageSize}
previousPage={previousPage}
nextPage={hasToken(pageNumber + 1) ? nextPage : undefined}
/>
)
}

Persistence.propTypes = {}
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
></script>
<script type="text/babel" src="components/ControlledImperative.js"></script>
<script type="text/babel" src="components/Uncontrolled.js"></script>
<script type="text/babel" src="components/Persistence.js"></script>
<script type="text/babel" src="components/Example.js"></script>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
// cacheDirectory: "C:\\Users\\simone\\AppData\\Local\\Temp\\jest",

// Automatically clear mock calls and instances between every test
clearMocks: true,
// clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
Expand Down
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
dir: 'cjs',
format: 'cjs',
exports: 'default',
esModule: false,
},
{
dir: 'es',
Expand Down
18 changes: 16 additions & 2 deletions src/controlled.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { useCallback, useEffect, useState, useMemo } from 'react'
import { NULL_PERSISTER } from './persisters'
import { assertNumber } from './utils'

export default function useControlledTokenPagination(pageNumber) {
const DEFAULTS = {
persister: NULL_PERSISTER,
}

export default function useControlledTokenPagination(pageNumber, options) {
options = { ...DEFAULTS, ...options }

assertNumber('pageNumber', pageNumber)

const [mapping, setMapping] = useState({})
const [mapping, setMapping] = useState(() => {
const { mapping } = options.persister.hydrate()
return mapping || {}
})

const updateToken = useCallback(
nextToken => {
Expand All @@ -23,6 +33,10 @@ export default function useControlledTokenPagination(pageNumber) {
[updateToken]
)

useEffect(() => {
options.persister.persist({ mapping })
}, [options.persister, mapping])

return useMemo(
() => ({
currentToken: mapping[pageNumber],
Expand Down
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useControlledTokenPagination from './controlled'
import useUncontrolledTokenPagination from './uncontrolled'
import * as persisters from './persisters'

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

return variant(options)
}

Object.assign(useTokenPagination, persisters)
26 changes: 26 additions & 0 deletions src/persisters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const NULL_PERSISTER = {
hydrate() {
return {}
},
persist() {},
}

function StoragePersister(key, storage) {
return {
hydrate() {
try {
return JSON.parse(storage.getItem(key) || '{}')
} catch (err) {
return {}
}
},
persist(value) {
storage.setItem(key, JSON.stringify({ ...this.hydrate(), ...value }))
},
}
}

export const localPersister = key =>
new StoragePersister(key, window.localStorage)
export const sessionPersister = key =>
new StoragePersister(key, window.sessionStorage)
54 changes: 54 additions & 0 deletions src/persisters.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { localPersister, sessionPersister } from './persisters'

describe('persisters', () => {
describe('local', persisterTests(localPersister, 'localStorage'))

describe('session', persisterTests(sessionPersister, 'sessionStorage'))
})

function persisterTests(persisterFactory, storageName) {
return function () {
let persister, storage

beforeEach(() => {
Object.defineProperty(window, storageName, {
value: { setItem: jest.fn(), getItem: jest.fn() },
})
storage = window[storageName]
persister = persisterFactory('key')
})

it('persists', () => {
persister.persist({ some: 'value' })

expect(storage.setItem).toHaveBeenCalledWith(
'key',
JSON.stringify({ some: 'value' })
)
})

it('hydrates', () => {
const stored = { some: 'value' }
storage.getItem.mockReturnValue(JSON.stringify(stored))

expect(persister.hydrate()).toEqual(stored)
})

it('merges with existing value when persisting', () => {
const stored = { some: 'value' }
storage.getItem.mockReturnValue(JSON.stringify(stored))

persister.persist({ another: 'value' })

expect(storage.setItem).toHaveBeenCalledWith(
'key',
JSON.stringify({ some: 'value', another: 'value' })
)
})

it('handles errors', () => {
storage.getItem.mockReturnValue('invalid json')
expect(persister.hydrate()).toEqual({})
})
}
}
51 changes: 32 additions & 19 deletions src/uncontrolled.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,67 @@
import { useCallback, useState, useMemo } from 'react'
import { useCallback, useState, useMemo, useEffect } from 'react'
import useControlledTokenPagination from './controlled'
import { NULL_PERSISTER } from './persisters'
import { assertNumber } from './utils'

const DEFAULTS = {
defaultPageNumber: 1,
resetPageNumberOnPageSizeChange: true,
persister: NULL_PERSISTER,
}

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

export default function useUncontrolledTokenPagination(options) {
options = { ...DEFAULTS, ...options }

assertNumber('defaultPageNumber', options.defaultPageNumber)
assertNumber('defaultPageSize', options.defaultPageSize)

const [{ pageNumber, pageSize }, setPagination] = useState({
pageNumber: options.defaultPageNumber,
pageSize: options.defaultPageSize,
const [{ pageNumber, pageSize }, setPagination] = useState(() => {
const { pageNumber, pageSize } = options.persister.hydrate()

return {
pageNumber: pageNumber || options.defaultPageNumber,
pageSize: pageSize || options.defaultPageSize,
}
})

const change = useCallback(
(property, changer) => {
const pageNumberReset = options.resetPageNumberOnPageSizeChange
? { pageNumber: options.defaultPageNumber }
: {}
: null

const changerType = typeof changer

if (!['function', 'number'].includes(changerType)) {
if (!changerTypes.includes(changerType)) {
throw new Error(
`Unsupported value ${changer} of type ${changerType} for ${property}`
`Unsupported value ${changer} of type ${changerType} for ${property}. Supported values are ${changerTypes}`
)
}

const paginate = p => {
return {
...p,
...pageNumberReset,
[property]: changerType === 'number' ? changer : changer(p[property]),
}
}

setPagination(paginate)
setPagination(p => ({
...p,
...pageNumberReset,
[property]: changerType === 'function' ? changer(p[property]) : changer,
}))
},
[options.defaultPageNumber, options.resetPageNumberOnPageSizeChange]
)

const changePageNumber = useCallback(c => change('pageNumber', c), [change])
const changePageSize = useCallback(c => change('pageSize', c), [change])
const changePageNumber = useCallback(
changer => change('pageNumber', changer),
[change]
)
const changePageSize = useCallback(changer => change('pageSize', changer), [
change,
])

const controlled = useControlledTokenPagination(pageNumber, options)

const controlled = useControlledTokenPagination(pageNumber)
useEffect(() => {
options.persister.persist({ pageNumber, pageSize })
}, [options.persister, pageNumber, pageSize])

return useMemo(
() => ({
Expand Down

0 comments on commit d4a1880

Please sign in to comment.