diff --git a/services/web/frontend/js/shared/hooks/use-persisted-state.js b/services/web/frontend/js/shared/hooks/use-persisted-state.js index e009b013c7..c66247067e 100644 --- a/services/web/frontend/js/shared/hooks/use-persisted-state.js +++ b/services/web/frontend/js/shared/hooks/use-persisted-state.js @@ -1,7 +1,14 @@ -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect } from 'react' import localStorage from '../../infrastructure/local-storage' -function usePersistedState(key, defaultValue) { +/** + * @param {string} key + * @param {any} [defaultValue] + * @param {boolean} [listen] + * + * @returns {[any, function]} + */ +function usePersistedState(key, defaultValue, listen = false) { const [value, setValue] = useState(() => { return localStorage.getItem(key) ?? defaultValue }) @@ -24,6 +31,24 @@ function usePersistedState(key, defaultValue) { [key, defaultValue] ) + useEffect(() => { + if (listen) { + const listener = event => { + if (event.key === key) { + // note: this value is read via getItem rather than from event.newValue + // because getItem handles deserializing the JSON that's stored in localStorage. + setValue(localStorage.getItem(key)) + } + } + + window.addEventListener('storage', listener) + + return () => { + window.removeEventListener('storage', listener) + } + } + }, [key, listen]) + return [value, updateFunction] } diff --git a/services/web/test/frontend/shared/hooks/use-persisted-state.test.js b/services/web/test/frontend/shared/hooks/use-persisted-state.test.js index d75dc51655..32e3dd3bd5 100644 --- a/services/web/test/frontend/shared/hooks/use-persisted-state.test.js +++ b/services/web/test/frontend/shared/hooks/use-persisted-state.test.js @@ -138,4 +138,46 @@ describe('usePersistedState', function () { expect(localStorage.getItem(key)).to.equal('foobar') }) + + it('handles syncing values via storage event', async function () { + const key = 'test:sync' + localStorage.setItem(key, 'foo') + expect(window.Storage.prototype.setItem).to.have.callCount(1) + + // listen for storage events + const storageEventListener = sinon.stub() + window.addEventListener('storage', storageEventListener) + + const Test = () => { + const [value, setValue] = usePersistedState(key, 'bar', true) + + useEffect(() => { + setValue('baz') + }, [setValue]) + + return
{value}
+ } + + render() + + screen.getByText('baz') + + expect(window.Storage.prototype.getItem).to.have.callCount(1) + expect(window.Storage.prototype.removeItem).to.have.callCount(0) + expect(window.Storage.prototype.setItem).to.have.callCount(2) + + expect(localStorage.getItem(key)).to.equal('baz') + + expect(storageEventListener).to.have.callCount(0) + + // set the new value in localStorage + localStorage.setItem(key, 'cat') + + // dispatch a "storage" event and check that it's picked up by the hook + window.dispatchEvent(new StorageEvent('storage', { key })) + + await screen.findByText('cat') + + expect(storageEventListener).to.have.callCount(1) + }) })