mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
40eda2eaf9
commit
233ceb5356
2 changed files with 154 additions and 8 deletions
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue