Allow function as value for usePersistedState hook (#5131)

* Allow function value in usePersistedState
* Add tests for usePersistedState
* Use nullish coalescing to avoid calling getItem twice

GitOrigin-RevId: e0351addea904aefb7a402bff32689792b49fbbb
This commit is contained in:
Alf Eaton 2021-09-23 11:35:50 +01:00 committed by Copybot
parent 40eda2eaf9
commit 233ceb5356
2 changed files with 154 additions and 8 deletions

View file

@ -3,18 +3,23 @@ import localStorage from '../../infrastructure/local-storage'
function usePersistedState(key, defaultValue) {
const [value, setValue] = useState(() => {
const keyExists = localStorage.getItem(key) != null
return keyExists ? localStorage.getItem(key) : defaultValue
return localStorage.getItem(key) ?? defaultValue
})
const updateFunction = useCallback(
newValue => {
if (newValue === defaultValue) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, newValue)
}
setValue(newValue)
setValue(value => {
const actualNewValue =
typeof newValue === 'function' ? newValue(value) : newValue
if (actualNewValue === defaultValue) {
localStorage.removeItem(key)
} else {
localStorage.setItem(key, actualNewValue)
}
return actualNewValue
})
},
[key, defaultValue]
)

View file

@ -0,0 +1,141 @@
import sinon from 'sinon'
import { expect } from 'chai'
import { useEffect } from 'react'
import { render, screen } from '@testing-library/react'
import usePersistedState from '../../../../frontend/js/shared/hooks/use-persisted-state'
import localStorage from '../../../../frontend/js/infrastructure/local-storage'
describe('usePersistedState', function () {
beforeEach(function () {
sinon.spy(global.localStorage, 'getItem')
sinon.spy(global.localStorage, 'removeItem')
sinon.spy(global.localStorage, 'setItem')
})
afterEach(function () {
sinon.restore()
})
it('reads the value from localStorage', function () {
const key = 'test'
localStorage.setItem(key, 'foo')
expect(global.localStorage.setItem).to.have.callCount(1)
const Test = () => {
const [value] = usePersistedState(key)
return <div>{value}</div>
}
render(<Test />)
screen.getByText('foo')
expect(global.localStorage.getItem).to.have.callCount(1)
expect(global.localStorage.removeItem).to.have.callCount(0)
expect(global.localStorage.setItem).to.have.callCount(1)
expect(localStorage.getItem(key)).to.equal('foo')
})
it('uses the default value without storing anything', function () {
const key = 'test:default'
const Test = () => {
const [value] = usePersistedState(key, 'foo')
return <div>{value}</div>
}
render(<Test />)
screen.getByText('foo')
expect(global.localStorage.getItem).to.have.callCount(1)
expect(global.localStorage.removeItem).to.have.callCount(0)
expect(global.localStorage.setItem).to.have.callCount(0)
expect(localStorage.getItem(key)).to.be.null
})
it('stores the new value in localStorage', function () {
const key = 'test:store'
localStorage.setItem(key, 'foo')
expect(global.localStorage.setItem).to.have.callCount(1)
const Test = () => {
const [value, setValue] = usePersistedState(key, 'bar')
useEffect(() => {
setValue('baz')
}, [setValue])
return <div>{value}</div>
}
render(<Test />)
screen.getByText('baz')
expect(global.localStorage.getItem).to.have.callCount(1)
expect(global.localStorage.removeItem).to.have.callCount(0)
expect(global.localStorage.setItem).to.have.callCount(2)
expect(localStorage.getItem(key)).to.equal('baz')
})
it('removes the value from localStorage if it equals the default value', function () {
const key = 'test:store-default'
localStorage.setItem(key, 'foo')
expect(global.localStorage.setItem).to.have.callCount(1)
const Test = () => {
const [value, setValue] = usePersistedState(key, 'bar')
useEffect(() => {
// set a different value
setValue('baz')
expect(localStorage.getItem(key)).to.equal('baz')
// set the default value again
setValue('bar')
}, [setValue])
return <div>{value}</div>
}
render(<Test />)
screen.getByText('bar')
expect(global.localStorage.getItem).to.have.callCount(2)
expect(global.localStorage.removeItem).to.have.callCount(1)
expect(global.localStorage.setItem).to.have.callCount(2)
expect(localStorage.getItem(key)).to.be.null
})
it('handles function values', function () {
const key = 'test:store'
localStorage.setItem(key, 'foo')
expect(global.localStorage.setItem).to.have.callCount(1)
const Test = () => {
const [value, setValue] = usePersistedState(key)
useEffect(() => {
setValue(value => value + 'bar')
}, [setValue])
return <div>{value}</div>
}
render(<Test />)
screen.getByText('foobar')
expect(global.localStorage.getItem).to.have.callCount(1)
expect(global.localStorage.removeItem).to.have.callCount(0)
expect(global.localStorage.setItem).to.have.callCount(2)
expect(localStorage.getItem(key)).to.equal('foobar')
})
})