From e7919a039d6ecbed226226558b58fd1c470579c8 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 25 Oct 2022 13:04:05 +0200 Subject: [PATCH 01/14] Refactor: external store in useLocalStorage --- src/services/ExternalStore.ts | 4 +- .../__tests__/useLocalStorage.test.ts | 74 +++++++++++++++++++ src/services/local-storage/useLocalStorage.ts | 68 +++++++++++------ 3 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 src/services/local-storage/__tests__/useLocalStorage.test.ts diff --git a/src/services/ExternalStore.ts b/src/services/ExternalStore.ts index 7097a84e5c..81fd85e464 100644 --- a/src/services/ExternalStore.ts +++ b/src/services/ExternalStore.ts @@ -15,9 +15,9 @@ class ExternalStore { return this.store } - public readonly setStore = (value: T): void => { + public readonly setStore = (value: T | ((prevValue: T | undefined) => T | undefined)): void => { if (value !== this.store) { - this.store = value + this.store = value instanceof Function ? value(this.store) : value this.listeners.forEach((listener) => listener()) } } diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts new file mode 100644 index 0000000000..dee57d731d --- /dev/null +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -0,0 +1,74 @@ +import { renderHook, act, waitFor } from '@/tests/test-utils' +import useLocalStorage from '../useLocalStorage' + +describe('useLocalStorage', () => { + beforeEach(() => { + window.localStorage.clear() + const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + result.current[1](undefined) + }) + + it('should return the initial value', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + + expect(result.current[0]).toBe('test') + }) + + it('should set the value', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + const [value, setValue] = result.current + + expect(value).toBe('test') + + act(() => { + setValue('test2') + }) + + expect(result.current[0]).toBe('test2') + }) + + it('should set the value using a callback', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + const [value, setValue] = result.current + + expect(value).toBe('test') + + act(() => { + setValue('test2') + }) + + act(() => { + setValue((prevVal) => { + return prevVal === 'test2' ? 'test3' : 'wrong' + }) + }) + + expect(result.current[0]).toBe('test3') + }) + + it('should read from LS on initial call', () => { + localStorage.setItem('test-key', 'ls') + + const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) + + expect(result.current[0]).toBe('initial') + + waitFor(() => expect(result.current[0]).toBe('ls')) + }) + + it('should read the same storage between hook invocations', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'hello')) + + expect(result.current[0]).toBe('hello') + + const { result: otherResult } = renderHook(() => useLocalStorage('test-key', 'hello')) + + expect(otherResult.current[0]).toBe('hello') + + act(() => { + otherResult.current[1]('world') + }) + + expect(result.current[0]).toBe('world') + }) +}) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 94f3f68300..d39de4ca8a 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -1,33 +1,59 @@ -import type { Dispatch, SetStateAction } from 'react' -import { useState, useCallback, useEffect } from 'react' +import { useCallback, useEffect } from 'react' +import ExternalStore from '../ExternalStore' import local from './local' -const useLocalStorage = (key: string, initialState: T): [T, Dispatch>] => { - const [cache, setCache] = useState(initialState) +// This store is a record whose keys correspond to the keys of the local storage +// It's basically a global cache of the local storage +// When a value is updated, all instances of the hook below will be updated +const { setStore, useStore } = new ExternalStore>() - useEffect(() => { - const initialValue = local.getItem(key) - if (initialValue !== undefined) { - setCache(initialValue) - } - }, [setCache, key]) - - const setNewValue = useCallback( - (value: T | ((prevState: T) => T)) => { - setCache((prevState) => { - const newState = value instanceof Function ? value(prevState) : value - - if (newState !== prevState) { - local.setItem(key, newState) +// The setter accepts T or a function that takes the old value and returns T +// Mimics the behavior of useState +type Setter = (val: T | undefined | ((prevVal: T) => T | undefined)) => void + +const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { + // This is the setter that will be returned + // It will update the local storage and the cache + const setNewValue = useCallback>( + (value) => { + setStore((prevStore = {}) => { + const prevVal = prevStore[key] as T + const newVal = value instanceof Function ? value(prevVal) : value + + // Nothing to update + if (prevVal === newVal) { + return prevStore + } + + // Update the local storage + if (newVal === undefined) { + local.removeItem(key) + } else { + local.setItem(key, newVal) } - return newState + // Update the cache + return { + ...prevStore, + [key]: newVal, + } }) }, - [setCache, key], + [key], ) - return [cache, setNewValue] + // Read the initial local storage value and put it in the cache + useEffect(() => { + setNewValue((prevVal) => prevVal ?? local.getItem(key)) + }, [key, setNewValue]) + + // Get the current value from the cache, or directly from the local storage if it's not in the cache + // Fall back to the initial value if it's not in the local storage + const cache = useStore() + const cachedVal = cache?.[key] as T + const currentVal = cachedVal ?? initialValue + + return [currentVal, setNewValue] } export default useLocalStorage From 52e9a1f36f291d6b5faa537a0e0c5185023bc34b Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 25 Oct 2022 13:13:34 +0200 Subject: [PATCH 02/14] Comment --- src/services/local-storage/useLocalStorage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index d39de4ca8a..1a10051cd0 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -47,8 +47,7 @@ const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { setNewValue((prevVal) => prevVal ?? local.getItem(key)) }, [key, setNewValue]) - // Get the current value from the cache, or directly from the local storage if it's not in the cache - // Fall back to the initial value if it's not in the local storage + // Get the current value from the cache, or fall back to the initial value const cache = useStore() const cachedVal = cache?.[key] as T const currentVal = cachedVal ?? initialValue From e0f29918e2b691c62739e123ee859ae234eebc70 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 25 Oct 2022 15:18:56 +0200 Subject: [PATCH 03/14] Sync separately --- src/pages/_app.tsx | 2 + src/services/local-storage/useLocalStorage.ts | 39 ++++++++++++------- src/tests/test-utils.tsx | 3 ++ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index de4df34f4c..46239ed64a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -31,6 +31,7 @@ import useBeamer from '@/hooks/useBeamer' import ErrorBoundary from '@/components/common/ErrorBoundary' import createEmotionCache from '@/utils/createEmotionCache' import MetaTags from '@/components/common/MetaTags' +import { useSyncLocalStorage } from '@/services/local-storage/useLocalStorage' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -48,6 +49,7 @@ const InitApp = (): null => { useSafeNotifications() useTxPendingStatuses() useTxTracking() + useSyncLocalStorage() useBeamer() return null diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 1a10051cd0..65f403f0f6 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -12,6 +12,11 @@ const { setStore, useStore } = new ExternalStore>() type Setter = (val: T | undefined | ((prevVal: T) => T | undefined)) => void const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { + // Get the current value from the cache, or fall back to the initial value + const cache = useStore() + const cachedVal = cache?.[key] as T + const currentVal = cachedVal ?? initialValue + // This is the setter that will be returned // It will update the local storage and the cache const setNewValue = useCallback>( @@ -21,16 +26,7 @@ const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { const newVal = value instanceof Function ? value(prevVal) : value // Nothing to update - if (prevVal === newVal) { - return prevStore - } - - // Update the local storage - if (newVal === undefined) { - local.removeItem(key) - } else { - local.setItem(key, newVal) - } + if (prevVal === newVal) return prevStore // Update the cache return { @@ -47,12 +43,25 @@ const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { setNewValue((prevVal) => prevVal ?? local.getItem(key)) }, [key, setNewValue]) - // Get the current value from the cache, or fall back to the initial value - const cache = useStore() - const cachedVal = cache?.[key] as T - const currentVal = cachedVal ?? initialValue - return [currentVal, setNewValue] } export default useLocalStorage + +export const useSyncLocalStorage = () => { + const cache = useStore() + + useEffect(() => { + if (!cache) return + + // Update the local storage when the cache changes + Object.entries(cache).forEach(([key, value]) => { + // Update the local storage + if (value === undefined) { + local.removeItem(key) + } else { + local.setItem(key, value) + } + }) + }, [cache]) +} diff --git a/src/tests/test-utils.tsx b/src/tests/test-utils.tsx index 1109890902..9dbbdb7804 100644 --- a/src/tests/test-utils.tsx +++ b/src/tests/test-utils.tsx @@ -5,6 +5,7 @@ import { RouterContext } from 'next/dist/shared/lib/router-context' import { ThemeProvider } from '@mui/material/styles' import initTheme from '@/styles/theme' import type { RootState } from '@/store' +import { useSyncLocalStorage } from '@/services/local-storage/useLocalStorage' const mockRouter = (props: Partial = {}): NextRouter => ({ asPath: '/', @@ -39,6 +40,8 @@ const getProviders: (options: { function ProviderComponent({ children }) { const { StoreHydrator } = require('@/store') // require dynamically to reset the store + useSyncLocalStorage() + return ( From 173dc0246255b8aaa25bfb2e279f28275f153499 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Tue, 25 Oct 2022 16:07:12 +0200 Subject: [PATCH 04/14] Use the storage event for syncing --- .../SafeAppsInfoModal/useSafeAppsInfoModal.ts | 2 +- src/components/sidebar/DebugToggle/index.tsx | 2 +- .../permissions/useBrowserPermissions.ts | 2 +- .../permissions/useSafePermissions.ts | 2 +- src/hooks/useOwnedSafes.ts | 4 +- src/pages/_app.tsx | 2 - .../__tests__/useLocalStorage.test.ts | 16 ---- src/services/local-storage/useLocalStorage.ts | 84 +++++++++---------- src/tests/test-utils.tsx | 3 - 9 files changed, 47 insertions(+), 70 deletions(-) diff --git a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts index bec7e8a058..faa6c88625 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts +++ b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts @@ -31,7 +31,7 @@ const useSafeAppsInfoModal = ({ } => { const didMount = useRef(false) const chainId = useChainId() - const [modalInfo, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL, {}) + const [modalInfo = {}, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL, {}) useEffect(() => { if (!didMount.current) { diff --git a/src/components/sidebar/DebugToggle/index.tsx b/src/components/sidebar/DebugToggle/index.tsx index 84ca722941..dd7acae144 100644 --- a/src/components/sidebar/DebugToggle/index.tsx +++ b/src/components/sidebar/DebugToggle/index.tsx @@ -20,7 +20,7 @@ const DebugToggle = (): ReactElement => { setIsProdGateway(event.target.checked) setTimeout(() => { - location.reload() + //location.reload() }, 300) } diff --git a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts index 69e7e20f32..d1b868c3b4 100644 --- a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts +++ b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts @@ -22,7 +22,7 @@ type UseBrowserPermissionsReturnType = { } const useBrowserPermissions = (): UseBrowserPermissionsReturnType => { - const [permissions, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) const getPermissions = useCallback( (origin: string) => { diff --git a/src/hooks/safe-apps/permissions/useSafePermissions.ts b/src/hooks/safe-apps/permissions/useSafePermissions.ts index 394b727721..9b9c56c7d2 100644 --- a/src/hooks/safe-apps/permissions/useSafePermissions.ts +++ b/src/hooks/safe-apps/permissions/useSafePermissions.ts @@ -36,7 +36,7 @@ type UseSafePermissionsReturnType = { } const useSafePermissions = (): UseSafePermissionsReturnType => { - const [permissions, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) const [permissionsRequest, setPermissionsRequest] = useState() diff --git a/src/hooks/useOwnedSafes.ts b/src/hooks/useOwnedSafes.ts index e5bf3359d6..2469db500a 100644 --- a/src/hooks/useOwnedSafes.ts +++ b/src/hooks/useOwnedSafes.ts @@ -31,7 +31,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { setOwnedSafesCache((prev) => ({ ...prev, [walletAddress]: { - ...(prev[walletAddress] || {}), + ...(prev?.[walletAddress] || {}), [chainId]: ownedSafes.safes, }, })) @@ -43,7 +43,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { } }, [error]) - return ownedSafesCache[walletAddress || ''] ?? {} + return ownedSafesCache?.[walletAddress || ''] ?? {} } export default useOwnedSafes diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 46239ed64a..de4df34f4c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -31,7 +31,6 @@ import useBeamer from '@/hooks/useBeamer' import ErrorBoundary from '@/components/common/ErrorBoundary' import createEmotionCache from '@/utils/createEmotionCache' import MetaTags from '@/components/common/MetaTags' -import { useSyncLocalStorage } from '@/services/local-storage/useLocalStorage' const GATEWAY_URL = IS_PRODUCTION || cgwDebugStorage.get() ? GATEWAY_URL_PRODUCTION : GATEWAY_URL_STAGING @@ -49,7 +48,6 @@ const InitApp = (): null => { useSafeNotifications() useTxPendingStatuses() useTxTracking() - useSyncLocalStorage() useBeamer() return null diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index dee57d731d..82dc0f28c5 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -55,20 +55,4 @@ describe('useLocalStorage', () => { waitFor(() => expect(result.current[0]).toBe('ls')) }) - - it('should read the same storage between hook invocations', () => { - const { result } = renderHook(() => useLocalStorage('test-key', 'hello')) - - expect(result.current[0]).toBe('hello') - - const { result: otherResult } = renderHook(() => useLocalStorage('test-key', 'hello')) - - expect(otherResult.current[0]).toBe('hello') - - act(() => { - otherResult.current[1]('world') - }) - - expect(result.current[0]).toBe('world') - }) }) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 65f403f0f6..c94d544f3d 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -1,67 +1,65 @@ -import { useCallback, useEffect } from 'react' -import ExternalStore from '../ExternalStore' +import { useCallback, useEffect, useState } from 'react' import local from './local' -// This store is a record whose keys correspond to the keys of the local storage -// It's basically a global cache of the local storage -// When a value is updated, all instances of the hook below will be updated -const { setStore, useStore } = new ExternalStore>() - // The setter accepts T or a function that takes the old value and returns T // Mimics the behavior of useState -type Setter = (val: T | undefined | ((prevVal: T) => T | undefined)) => void +type Setter = (val: T | undefined | ((prevVal: T | undefined) => T | undefined)) => void -const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { - // Get the current value from the cache, or fall back to the initial value - const cache = useStore() - const cachedVal = cache?.[key] as T - const currentVal = cachedVal ?? initialValue +const useLocalStorage = (key: string, initialValue: T): [T | undefined, Setter] => { + const [cache, setCache] = useState(initialValue) // This is the setter that will be returned - // It will update the local storage and the cache + // It will update the local storage and cache const setNewValue = useCallback>( (value) => { - setStore((prevStore = {}) => { - const prevVal = prevStore[key] as T - const newVal = value instanceof Function ? value(prevVal) : value + setCache((oldValue) => { + const newValue = value instanceof Function ? value(oldValue) : value - // Nothing to update - if (prevVal === newVal) return prevStore + if (newValue !== oldValue) { + local.setItem(key, newValue) - // Update the cache - return { - ...prevStore, - [key]: newVal, + // Dispatch a fake storage event within the current browser tab + // The real storage event is dispatched only in other tabs + window.dispatchEvent( + new StorageEvent('storage', { + key: local.getPrefixedKey(key), + }), + ) } + + return newValue }) }, [key], ) - // Read the initial local storage value and put it in the cache + // Subscribe to changes in local storage and update the cache + // This will work across tabs useEffect(() => { - setNewValue((prevVal) => prevVal ?? local.getItem(key)) - }, [key, setNewValue]) + const onStorageEvent = (event: StorageEvent) => { + if (event.key === local.getPrefixedKey(key)) { + setCache(local.getItem(key)) + } + } - return [currentVal, setNewValue] -} + // Subscribe to storage events from othet tabs and this tab + window.addEventListener('storage', onStorageEvent) -export default useLocalStorage - -export const useSyncLocalStorage = () => { - const cache = useStore() + return () => { + window.removeEventListener('storage', onStorageEvent) + } + }, [key]) + // Set the initial value when the component is mounted + // Ignore undefined to avoid overwriting the externally passed inital value useEffect(() => { - if (!cache) return + const lsValue = local.getItem(key) + if (lsValue !== undefined) { + setCache(lsValue) + } + }, [key]) - // Update the local storage when the cache changes - Object.entries(cache).forEach(([key, value]) => { - // Update the local storage - if (value === undefined) { - local.removeItem(key) - } else { - local.setItem(key, value) - } - }) - }, [cache]) + return [cache, setNewValue] } + +export default useLocalStorage diff --git a/src/tests/test-utils.tsx b/src/tests/test-utils.tsx index 9dbbdb7804..1109890902 100644 --- a/src/tests/test-utils.tsx +++ b/src/tests/test-utils.tsx @@ -5,7 +5,6 @@ import { RouterContext } from 'next/dist/shared/lib/router-context' import { ThemeProvider } from '@mui/material/styles' import initTheme from '@/styles/theme' import type { RootState } from '@/store' -import { useSyncLocalStorage } from '@/services/local-storage/useLocalStorage' const mockRouter = (props: Partial = {}): NextRouter => ({ asPath: '/', @@ -40,8 +39,6 @@ const getProviders: (options: { function ProviderComponent({ children }) { const { StoreHydrator } = require('@/store') // require dynamically to reset the store - useSyncLocalStorage() - return ( From 23a68d51d9df0ff5cea7f585b95f6646a2d5f99d Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 10:11:38 +0200 Subject: [PATCH 05/14] Rm comment --- src/components/sidebar/DebugToggle/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sidebar/DebugToggle/index.tsx b/src/components/sidebar/DebugToggle/index.tsx index dd7acae144..84ca722941 100644 --- a/src/components/sidebar/DebugToggle/index.tsx +++ b/src/components/sidebar/DebugToggle/index.tsx @@ -20,7 +20,7 @@ const DebugToggle = (): ReactElement => { setIsProdGateway(event.target.checked) setTimeout(() => { - //location.reload() + location.reload() }, 300) } From 9ae1d240c8fe6cd200cd58deb809013d7b3e0dbd Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 10:18:04 +0200 Subject: [PATCH 06/14] Restore prev behavior wrt undefined --- .../SafeAppsInfoModal/useSafeAppsInfoModal.ts | 2 +- .../safe-apps/permissions/useBrowserPermissions.ts | 2 +- src/hooks/safe-apps/permissions/useSafePermissions.ts | 2 +- src/hooks/useOwnedSafes.ts | 4 ++-- src/services/ExternalStore.ts | 4 ++-- .../local-storage/__tests__/useLocalStorage.test.ts | 2 +- src/services/local-storage/useLocalStorage.ts | 11 +++++++---- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts index faa6c88625..bec7e8a058 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts +++ b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts @@ -31,7 +31,7 @@ const useSafeAppsInfoModal = ({ } => { const didMount = useRef(false) const chainId = useChainId() - const [modalInfo = {}, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL, {}) + const [modalInfo, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL, {}) useEffect(() => { if (!didMount.current) { diff --git a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts index d1b868c3b4..69e7e20f32 100644 --- a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts +++ b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts @@ -22,7 +22,7 @@ type UseBrowserPermissionsReturnType = { } const useBrowserPermissions = (): UseBrowserPermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) + const [permissions, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) const getPermissions = useCallback( (origin: string) => { diff --git a/src/hooks/safe-apps/permissions/useSafePermissions.ts b/src/hooks/safe-apps/permissions/useSafePermissions.ts index 9b9c56c7d2..394b727721 100644 --- a/src/hooks/safe-apps/permissions/useSafePermissions.ts +++ b/src/hooks/safe-apps/permissions/useSafePermissions.ts @@ -36,7 +36,7 @@ type UseSafePermissionsReturnType = { } const useSafePermissions = (): UseSafePermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) + const [permissions, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) const [permissionsRequest, setPermissionsRequest] = useState() diff --git a/src/hooks/useOwnedSafes.ts b/src/hooks/useOwnedSafes.ts index 2469db500a..e5bf3359d6 100644 --- a/src/hooks/useOwnedSafes.ts +++ b/src/hooks/useOwnedSafes.ts @@ -31,7 +31,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { setOwnedSafesCache((prev) => ({ ...prev, [walletAddress]: { - ...(prev?.[walletAddress] || {}), + ...(prev[walletAddress] || {}), [chainId]: ownedSafes.safes, }, })) @@ -43,7 +43,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { } }, [error]) - return ownedSafesCache?.[walletAddress || ''] ?? {} + return ownedSafesCache[walletAddress || ''] ?? {} } export default useOwnedSafes diff --git a/src/services/ExternalStore.ts b/src/services/ExternalStore.ts index 81fd85e464..7097a84e5c 100644 --- a/src/services/ExternalStore.ts +++ b/src/services/ExternalStore.ts @@ -15,9 +15,9 @@ class ExternalStore { return this.store } - public readonly setStore = (value: T | ((prevValue: T | undefined) => T | undefined)): void => { + public readonly setStore = (value: T): void => { if (value !== this.store) { - this.store = value instanceof Function ? value(this.store) : value + this.store = value this.listeners.forEach((listener) => listener()) } } diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index 82dc0f28c5..be288a2abe 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -5,7 +5,7 @@ describe('useLocalStorage', () => { beforeEach(() => { window.localStorage.clear() const { result } = renderHook(() => useLocalStorage('test-key', 'test')) - result.current[1](undefined) + result.current[1](undefined as any) }) it('should return the initial value', () => { diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index c94d544f3d..91578d0b39 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -3,10 +3,10 @@ import local from './local' // The setter accepts T or a function that takes the old value and returns T // Mimics the behavior of useState -type Setter = (val: T | undefined | ((prevVal: T | undefined) => T | undefined)) => void +type Setter = (val: T | ((prevVal: T) => T)) => void -const useLocalStorage = (key: string, initialValue: T): [T | undefined, Setter] => { - const [cache, setCache] = useState(initialValue) +const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { + const [cache, setCache] = useState(initialValue) // This is the setter that will be returned // It will update the local storage and cache @@ -38,7 +38,10 @@ const useLocalStorage = (key: string, initialValue: T): [T | undefined, Sette useEffect(() => { const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - setCache(local.getItem(key)) + const newValue = local.getItem(key) + setCache((oldValue) => { + return newValue === undefined || newValue === oldValue ? oldValue : newValue + }) } } From 347a484f947176ca09fa688e2a0b1f5fbe39259a Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 13:24:45 +0200 Subject: [PATCH 07/14] Simplify the undefined check --- .../local-storage/__tests__/useLocalStorage.test.ts | 2 -- src/services/local-storage/useLocalStorage.ts | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index be288a2abe..3c76bb17ee 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -4,8 +4,6 @@ import useLocalStorage from '../useLocalStorage' describe('useLocalStorage', () => { beforeEach(() => { window.localStorage.clear() - const { result } = renderHook(() => useLocalStorage('test-key', 'test')) - result.current[1](undefined as any) }) it('should return the initial value', () => { diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 91578d0b39..ed8d0c6347 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -39,9 +39,9 @@ const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { const newValue = local.getItem(key) - setCache((oldValue) => { - return newValue === undefined || newValue === oldValue ? oldValue : newValue - }) + if (newValue !== undefined) { + setCache(newValue) + } } } From 43f7a3d9ed40f4a7559b10e3352b59deef93675e Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 14:12:39 +0200 Subject: [PATCH 08/14] Allow undefined and rm initialValue --- src/components/create-safe/usePendingSafe.ts | 1 - .../SafeAppsInfoModal/useSafeAppsInfoModal.ts | 2 +- src/components/sidebar/DebugToggle/index.tsx | 2 +- .../permissions/useBrowserPermissions.ts | 2 +- .../permissions/useSafePermissions.ts | 2 +- src/hooks/useOwnedSafes.ts | 6 ++--- .../__tests__/useLocalStorage.test.ts | 24 +++++++++---------- src/services/local-storage/useLocalStorage.ts | 11 ++++----- src/services/ls-migration/index.ts | 2 +- 9 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/components/create-safe/usePendingSafe.ts b/src/components/create-safe/usePendingSafe.ts index f3c794c352..1405e49448 100644 --- a/src/components/create-safe/usePendingSafe.ts +++ b/src/components/create-safe/usePendingSafe.ts @@ -12,7 +12,6 @@ export const usePendingSafe = (): [Props, Dispatch>] => { const chainId = useChainId() const [pendingSafes, setPendingSafes] = useLocalStorage( SAFE_PENDING_CREATION_STORAGE_KEY, - undefined, ) const pendingSafe = pendingSafes?.[chainId] diff --git a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts index bec7e8a058..96604a5a88 100644 --- a/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts +++ b/src/components/safe-apps/SafeAppsInfoModal/useSafeAppsInfoModal.ts @@ -31,7 +31,7 @@ const useSafeAppsInfoModal = ({ } => { const didMount = useRef(false) const chainId = useChainId() - const [modalInfo, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL, {}) + const [modalInfo = {}, setModalInfo] = useLocalStorage(SAFE_APPS_INFO_MODAL) useEffect(() => { if (!didMount.current) { diff --git a/src/components/sidebar/DebugToggle/index.tsx b/src/components/sidebar/DebugToggle/index.tsx index 84ca722941..27a2a201ae 100644 --- a/src/components/sidebar/DebugToggle/index.tsx +++ b/src/components/sidebar/DebugToggle/index.tsx @@ -14,7 +14,7 @@ const DebugToggle = (): ReactElement => { const dispatch = useAppDispatch() const isDarkMode = useDarkMode() - const [isProdGateway, setIsProdGateway] = useLocalStorage(LS_KEY, false) + const [isProdGateway = false, setIsProdGateway] = useLocalStorage(LS_KEY) const onToggle = (event: ChangeEvent) => { setIsProdGateway(event.target.checked) diff --git a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts index 69e7e20f32..cabbd30b20 100644 --- a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts +++ b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts @@ -22,7 +22,7 @@ type UseBrowserPermissionsReturnType = { } const useBrowserPermissions = (): UseBrowserPermissionsReturnType => { - const [permissions, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS) const getPermissions = useCallback( (origin: string) => { diff --git a/src/hooks/safe-apps/permissions/useSafePermissions.ts b/src/hooks/safe-apps/permissions/useSafePermissions.ts index 394b727721..5c67325111 100644 --- a/src/hooks/safe-apps/permissions/useSafePermissions.ts +++ b/src/hooks/safe-apps/permissions/useSafePermissions.ts @@ -36,7 +36,7 @@ type UseSafePermissionsReturnType = { } const useSafePermissions = (): UseSafePermissionsReturnType => { - const [permissions, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS) const [permissionsRequest, setPermissionsRequest] = useState() diff --git a/src/hooks/useOwnedSafes.ts b/src/hooks/useOwnedSafes.ts index e5bf3359d6..45eebefae1 100644 --- a/src/hooks/useOwnedSafes.ts +++ b/src/hooks/useOwnedSafes.ts @@ -18,7 +18,7 @@ type OwnedSafesCache = { const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { const chainId = useChainId() const { address: walletAddress } = useWallet() || {} - const [ownedSafesCache, setOwnedSafesCache] = useLocalStorage(CACHE_KEY, {}) + const [ownedSafesCache, setOwnedSafesCache] = useLocalStorage(CACHE_KEY) const [ownedSafes, error] = useAsync(() => { if (!chainId || !walletAddress) return @@ -31,7 +31,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { setOwnedSafesCache((prev) => ({ ...prev, [walletAddress]: { - ...(prev[walletAddress] || {}), + ...(prev?.[walletAddress] || {}), [chainId]: ownedSafes.safes, }, })) @@ -43,7 +43,7 @@ const useOwnedSafes = (): OwnedSafesCache['walletAddress'] => { } }, [error]) - return ownedSafesCache[walletAddress || ''] ?? {} + return ownedSafesCache?.[walletAddress || ''] ?? {} } export default useOwnedSafes diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index 3c76bb17ee..cddd9044c6 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -6,17 +6,17 @@ describe('useLocalStorage', () => { window.localStorage.clear() }) - it('should return the initial value', () => { - const { result } = renderHook(() => useLocalStorage('test-key', 'test')) - - expect(result.current[0]).toBe('test') - }) - it('should set the value', () => { - const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + const { result } = renderHook(() => useLocalStorage('test-key')) const [value, setValue] = result.current - expect(value).toBe('test') + expect(value).toBe(undefined) + + act(() => { + setValue('test') + }) + + expect(result.current[0]).toBe('test') act(() => { setValue('test2') @@ -26,10 +26,10 @@ describe('useLocalStorage', () => { }) it('should set the value using a callback', () => { - const { result } = renderHook(() => useLocalStorage('test-key', 'test')) + const { result } = renderHook(() => useLocalStorage('test-key')) const [value, setValue] = result.current - expect(value).toBe('test') + expect(value).toBe(undefined) act(() => { setValue('test2') @@ -47,9 +47,9 @@ describe('useLocalStorage', () => { it('should read from LS on initial call', () => { localStorage.setItem('test-key', 'ls') - const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) + const { result } = renderHook(() => useLocalStorage('test-key')) - expect(result.current[0]).toBe('initial') + expect(result.current[0]).toBe(undefined) waitFor(() => expect(result.current[0]).toBe('ls')) }) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index ed8d0c6347..0bdb5dae8b 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -3,10 +3,10 @@ import local from './local' // The setter accepts T or a function that takes the old value and returns T // Mimics the behavior of useState -type Setter = (val: T | ((prevVal: T) => T)) => void +type Setter = (val: (T | undefined) | ((prevVal: T | undefined) => T | undefined)) => void -const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { - const [cache, setCache] = useState(initialValue) +const useLocalStorage = (key: string): [T | undefined, Setter] => { + const [cache, setCache] = useState() // This is the setter that will be returned // It will update the local storage and cache @@ -38,10 +38,7 @@ const useLocalStorage = (key: string, initialValue: T): [T, Setter] => { useEffect(() => { const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - const newValue = local.getItem(key) - if (newValue !== undefined) { - setCache(newValue) - } + setCache(local.getItem(key)) } } diff --git a/src/services/ls-migration/index.ts b/src/services/ls-migration/index.ts index 7db5a7c652..8ad4139b92 100644 --- a/src/services/ls-migration/index.ts +++ b/src/services/ls-migration/index.ts @@ -12,7 +12,7 @@ import { MIGRATION_KEY } from './config' const useStorageMigration = (): void => { const dispatch = useAppDispatch() - const [isMigrationFinished, setIsMigrationFinished] = useLocalStorage(MIGRATION_KEY, false) + const [isMigrationFinished = false, setIsMigrationFinished] = useLocalStorage(MIGRATION_KEY) useEffect(() => { if (isMigrationFinished) return From efdce6ae0279e476e53f027a95653ab94200f77a Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 16:33:48 +0200 Subject: [PATCH 09/14] Don't set undefined --- src/services/local-storage/useLocalStorage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 0bdb5dae8b..9d4e96fd79 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -38,7 +38,10 @@ const useLocalStorage = (key: string): [T | undefined, Setter] => { useEffect(() => { const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - setCache(local.getItem(key)) + const newValue = local.getItem(key) + if (newValue !== undefined) { + setCache(newValue) + } } } From 08e04b921bf009f7d466aa95e12f83ef33c6720d Mon Sep 17 00:00:00 2001 From: katspaugh Date: Wed, 26 Oct 2022 18:13:02 +0200 Subject: [PATCH 10/14] Local storage to return null when not found --- src/services/local-storage/Storage.ts | 13 +++++-- .../local-storage/__tests__/local.test.ts | 10 ++--- src/services/local-storage/useLocalStorage.ts | 37 +++++++++---------- src/services/ls-migration/index.ts | 2 +- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/services/local-storage/Storage.ts b/src/services/local-storage/Storage.ts index 229fc3ddf1..3bbce8b64d 100644 --- a/src/services/local-storage/Storage.ts +++ b/src/services/local-storage/Storage.ts @@ -21,28 +21,33 @@ class Storage { return `${this.prefix}${key}` } - public getItem = (key: string): T | undefined => { + public getItem = (key: string): T | null => { const fullKey = this.getPrefixedKey(key) let saved: string | null = null try { - saved = this.storage?.getItem(fullKey) || null + saved = this.storage?.getItem(fullKey) ?? null } catch (err) { logError(Errors._700, `key ${key} – ${(err as Error).message}`) } - if (!saved || saved === 'undefined') return + if (saved == null) return null try { return JSON.parse(saved) as T } catch (err) { logError(Errors._700, `key ${key} – ${(err as Error).message}`) } + return null } public setItem = (key: string, item: T): void => { const fullKey = this.getPrefixedKey(key) try { - this.storage?.setItem(fullKey, JSON.stringify(item)) + if (item == null) { + this.storage?.removeItem(fullKey) + } else { + this.storage?.setItem(fullKey, JSON.stringify(item)) + } } catch (err) { logError(Errors._701, `key ${key} – ${(err as Error).message}`) } diff --git a/src/services/local-storage/__tests__/local.test.ts b/src/services/local-storage/__tests__/local.test.ts index b02766ac5c..ea53cda797 100644 --- a/src/services/local-storage/__tests__/local.test.ts +++ b/src/services/local-storage/__tests__/local.test.ts @@ -18,8 +18,8 @@ describe('local storage', () => { expect(getItem('test')).toStrictEqual({ test: 'value' }) }) - it("returns undefined the key doesn't exist", () => { - expect(getItem('notAKey')).toBe(undefined) + it("returns null of the key doesn't exist", () => { + expect(getItem('notAKey')).toBe(null) }) }) @@ -32,10 +32,10 @@ describe('local storage', () => { }) describe('handling undefined', () => { - it('saves ands reads undefined', () => { + it('removes the key when passing undefined', () => { setItem('test_undefined', undefined) - expect(getItem('test_undefined')).toBe(undefined) - expect(window.localStorage.getItem('SAFE_v2__test_undefined')).toBe('undefined') + expect(getItem('test_undefined')).toBe(null) + expect(window.localStorage.getItem('SAFE_v2__test_undefined')).toBe(null) }) }) }) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 9d4e96fd79..340f2bbd74 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -3,10 +3,12 @@ import local from './local' // The setter accepts T or a function that takes the old value and returns T // Mimics the behavior of useState -type Setter = (val: (T | undefined) | ((prevVal: T | undefined) => T | undefined)) => void +type Undefinable = T | undefined -const useLocalStorage = (key: string): [T | undefined, Setter] => { - const [cache, setCache] = useState() +type Setter = (val: T | ((prevVal: Undefinable) => Undefinable)) => void + +const useLocalStorage = (key: string): [Undefinable, Setter] => { + const [cache, setCache] = useState>() // This is the setter that will be returned // It will update the local storage and cache @@ -33,19 +35,25 @@ const useLocalStorage = (key: string): [T | undefined, Setter] => { [key], ) - // Subscribe to changes in local storage and update the cache - // This will work across tabs useEffect(() => { + const syncCache = () => { + const lsValue = local.getItem(key) + if (lsValue !== null) { + setCache(lsValue) + } + } + + // Subscribe to changes in local storage and update the cache + // This will work across tabs const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - const newValue = local.getItem(key) - if (newValue !== undefined) { - setCache(newValue) - } + syncCache() } } - // Subscribe to storage events from othet tabs and this tab + // Set the initial value when the component is mounted + syncCache() + window.addEventListener('storage', onStorageEvent) return () => { @@ -53,15 +61,6 @@ const useLocalStorage = (key: string): [T | undefined, Setter] => { } }, [key]) - // Set the initial value when the component is mounted - // Ignore undefined to avoid overwriting the externally passed inital value - useEffect(() => { - const lsValue = local.getItem(key) - if (lsValue !== undefined) { - setCache(lsValue) - } - }, [key]) - return [cache, setNewValue] } diff --git a/src/services/ls-migration/index.ts b/src/services/ls-migration/index.ts index 8ad4139b92..0e68c27954 100644 --- a/src/services/ls-migration/index.ts +++ b/src/services/ls-migration/index.ts @@ -12,7 +12,7 @@ import { MIGRATION_KEY } from './config' const useStorageMigration = (): void => { const dispatch = useAppDispatch() - const [isMigrationFinished = false, setIsMigrationFinished] = useLocalStorage(MIGRATION_KEY) + const [isMigrationFinished = false, setIsMigrationFinished] = useLocalStorage(MIGRATION_KEY) useEffect(() => { if (isMigrationFinished) return From 91ddc785355fe1ab11a8a5b1308446f60cf42522 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Thu, 27 Oct 2022 11:11:31 +0200 Subject: [PATCH 11/14] Initial value --- .../permissions/useBrowserPermissions.ts | 2 +- .../permissions/useSafePermissions.ts | 2 +- .../__tests__/useLocalStorage.test.ts | 23 ++++++++++--- src/services/local-storage/useLocalStorage.ts | 32 +++++++++++-------- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts index cabbd30b20..d1b868c3b4 100644 --- a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts +++ b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts @@ -22,7 +22,7 @@ type UseBrowserPermissionsReturnType = { } const useBrowserPermissions = (): UseBrowserPermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS) + const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) const getPermissions = useCallback( (origin: string) => { diff --git a/src/hooks/safe-apps/permissions/useSafePermissions.ts b/src/hooks/safe-apps/permissions/useSafePermissions.ts index 5c67325111..9b9c56c7d2 100644 --- a/src/hooks/safe-apps/permissions/useSafePermissions.ts +++ b/src/hooks/safe-apps/permissions/useSafePermissions.ts @@ -36,7 +36,7 @@ type UseSafePermissionsReturnType = { } const useSafePermissions = (): UseSafePermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS) + const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) const [permissionsRequest, setPermissionsRequest] = useState() diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index cddd9044c6..e85bc22e26 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -1,4 +1,5 @@ -import { renderHook, act, waitFor } from '@/tests/test-utils' +import { renderHook, act } from '@/tests/test-utils' +import local from '../local' import useLocalStorage from '../useLocalStorage' describe('useLocalStorage', () => { @@ -45,12 +46,26 @@ describe('useLocalStorage', () => { }) it('should read from LS on initial call', () => { - localStorage.setItem('test-key', 'ls') + local.setItem('test-key', 'ls') const { result } = renderHook(() => useLocalStorage('test-key')) - expect(result.current[0]).toBe(undefined) + expect(result.current[0]).toBe('ls') + }) + + it('should save the initial value to the LS', () => { + const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) + + expect(result.current[0]).toBe('initial') + expect(local.getItem('test-key')).toBe('initial') + }) + + it('should NOT save the initial value to the LS if LS is already populated', () => { + local.setItem('test-key', 'ls') + + const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) - waitFor(() => expect(result.current[0]).toBe('ls')) + expect(result.current[0]).toBe('ls') + expect(local.getItem('test-key')).toBe('ls') }) }) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 340f2bbd74..45abdac7b7 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -7,8 +7,8 @@ type Undefinable = T | undefined type Setter = (val: T | ((prevVal: Undefinable) => Undefinable)) => void -const useLocalStorage = (key: string): [Undefinable, Setter] => { - const [cache, setCache] = useState>() +const useLocalStorage = (key: string, initialValue?: T): [Undefinable, Setter] => { + const [cache, setCache] = useState>(initialValue) // This is the setter that will be returned // It will update the local storage and cache @@ -35,25 +35,31 @@ const useLocalStorage = (key: string): [Undefinable, Setter] => { [key], ) + // On mount, sync the cache with the local storage useEffect(() => { - const syncCache = () => { - const lsValue = local.getItem(key) - if (lsValue !== null) { - setCache(lsValue) - } + const lsValue = local.getItem(key) + if (lsValue !== null) { + setCache(lsValue) } + }, [key]) - // Subscribe to changes in local storage and update the cache - // This will work across tabs + // Save initial value to the LS + useEffect(() => { + if (initialValue !== undefined && local.getItem(key) === null) { + local.setItem(key, initialValue) + } + }, [key, initialValue]) + + // Subscribe to changes in local storage and update the cache + // This will work across tabs + useEffect(() => { const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - syncCache() + const lsValue = local.getItem(key) + setCache(lsValue ?? undefined) } } - // Set the initial value when the component is mounted - syncCache() - window.addEventListener('storage', onStorageEvent) return () => { From ce9b97d2e4414e7a18104e08be9b08a84561bdf2 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Thu, 27 Oct 2022 13:48:23 +0200 Subject: [PATCH 12/14] Rm initial value again --- .../permissions/useBrowserPermissions.ts | 2 +- .../permissions/useSafePermissions.ts | 2 +- .../__tests__/useLocalStorage.test.ts | 16 ------------- src/services/local-storage/useLocalStorage.ts | 24 ++++++++++--------- 4 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts index d1b868c3b4..cabbd30b20 100644 --- a/src/hooks/safe-apps/permissions/useBrowserPermissions.ts +++ b/src/hooks/safe-apps/permissions/useBrowserPermissions.ts @@ -22,7 +22,7 @@ type UseBrowserPermissionsReturnType = { } const useBrowserPermissions = (): UseBrowserPermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(BROWSER_PERMISSIONS) const getPermissions = useCallback( (origin: string) => { diff --git a/src/hooks/safe-apps/permissions/useSafePermissions.ts b/src/hooks/safe-apps/permissions/useSafePermissions.ts index 9b9c56c7d2..5c67325111 100644 --- a/src/hooks/safe-apps/permissions/useSafePermissions.ts +++ b/src/hooks/safe-apps/permissions/useSafePermissions.ts @@ -36,7 +36,7 @@ type UseSafePermissionsReturnType = { } const useSafePermissions = (): UseSafePermissionsReturnType => { - const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS, {}) + const [permissions = {}, setPermissions] = useLocalStorage(SAFE_PERMISSIONS) const [permissionsRequest, setPermissionsRequest] = useState() diff --git a/src/services/local-storage/__tests__/useLocalStorage.test.ts b/src/services/local-storage/__tests__/useLocalStorage.test.ts index e85bc22e26..9639045783 100644 --- a/src/services/local-storage/__tests__/useLocalStorage.test.ts +++ b/src/services/local-storage/__tests__/useLocalStorage.test.ts @@ -52,20 +52,4 @@ describe('useLocalStorage', () => { expect(result.current[0]).toBe('ls') }) - - it('should save the initial value to the LS', () => { - const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) - - expect(result.current[0]).toBe('initial') - expect(local.getItem('test-key')).toBe('initial') - }) - - it('should NOT save the initial value to the LS if LS is already populated', () => { - local.setItem('test-key', 'ls') - - const { result } = renderHook(() => useLocalStorage('test-key', 'initial')) - - expect(result.current[0]).toBe('ls') - expect(local.getItem('test-key')).toBe('ls') - }) }) diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 45abdac7b7..2856aa5eeb 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -7,8 +7,8 @@ type Undefinable = T | undefined type Setter = (val: T | ((prevVal: Undefinable) => Undefinable)) => void -const useLocalStorage = (key: string, initialValue?: T): [Undefinable, Setter] => { - const [cache, setCache] = useState>(initialValue) +const useLocalStorage = (key: string): [Undefinable, Setter] => { + const [cache, setCache] = useState>() // This is the setter that will be returned // It will update the local storage and cache @@ -43,23 +43,25 @@ const useLocalStorage = (key: string, initialValue?: T): [Undefinable, Set } }, [key]) - // Save initial value to the LS - useEffect(() => { - if (initialValue !== undefined && local.getItem(key) === null) { - local.setItem(key, initialValue) - } - }, [key, initialValue]) - // Subscribe to changes in local storage and update the cache // This will work across tabs useEffect(() => { + const syncCache = () => { + const lsValue = local.getItem(key) + if (lsValue !== null) { + setCache(lsValue) + } + } + const onStorageEvent = (event: StorageEvent) => { if (event.key === local.getPrefixedKey(key)) { - const lsValue = local.getItem(key) - setCache(lsValue ?? undefined) + syncCache() } } + // Set the initial value + syncCache() + window.addEventListener('storage', onStorageEvent) return () => { From 17cd2d8ea77f94f1e7dff08ae204d20df09e9588 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 31 Oct 2022 08:26:45 +0100 Subject: [PATCH 13/14] usePendingCreation --- src/components/create-safe/status/CreationStatus.tsx | 7 ++----- src/components/create-safe/steps/ReviewStep.tsx | 6 ++---- src/components/create-safe/usePendingCreation.ts | 10 ++++++++++ src/components/create-safe/useSetCreationStep.ts | 8 +++----- yarn.lock | 9 ++++++++- 5 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 src/components/create-safe/usePendingCreation.ts diff --git a/src/components/create-safe/status/CreationStatus.tsx b/src/components/create-safe/status/CreationStatus.tsx index b0f4cd1841..9b45884e20 100644 --- a/src/components/create-safe/status/CreationStatus.tsx +++ b/src/components/create-safe/status/CreationStatus.tsx @@ -12,11 +12,11 @@ import { useCallback } from 'react' import { useSafeCreation } from '@/components/create-safe/status/useSafeCreation' import type { StepRenderProps } from '@/components/tx/TxStepper/useTxStepper' import type { PendingSafeData } from '@/components/create-safe/types.d' -import useLocalStorage from '@/services/local-storage/useLocalStorage' import StatusMessage from '@/components/create-safe/status/StatusMessage' import useWallet from '@/hooks/wallets/useWallet' import useIsWrongChain from '@/hooks/useIsWrongChain' import useStatus from '@/components/create-safe/status/useStatus' +import usePendingCreation from '@/components/create-safe/usePendingCreation' export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' @@ -29,10 +29,7 @@ type Props = { export const CreationStatus = ({ params, setStep }: Props) => { const [status, setStatus] = useStatus() - const [pendingSafe, setPendingSafe] = useLocalStorage( - SAFE_PENDING_CREATION_STORAGE_KEY, - params, - ) + const [pendingSafe = params, setPendingSafe] = usePendingCreation() const wallet = useWallet() const isWrongChain = useIsWrongChain() const isConnected = wallet && !isWrongChain diff --git a/src/components/create-safe/steps/ReviewStep.tsx b/src/components/create-safe/steps/ReviewStep.tsx index 2cbe975381..e97d2046e2 100644 --- a/src/components/create-safe/steps/ReviewStep.tsx +++ b/src/components/create-safe/steps/ReviewStep.tsx @@ -13,9 +13,7 @@ import { getFallbackHandlerContractInstance } from '@/services/contracts/safeCon import { computeNewSafeAddress } from '@/components/create-safe/logic' import { useWeb3 } from '@/hooks/wallets/web3' import useWallet from '@/hooks/wallets/useWallet' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import type { PendingSafeData } from '@/components/create-safe/types' -import { SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/create-safe/status/CreationStatus' +import usePendingCreation from '../usePendingCreation' type Props = { params: SafeFormData @@ -30,7 +28,7 @@ const ReviewStep = ({ params, onSubmit, setStep, onBack }: Props) => { const provider = useWeb3() const chain = useCurrentChain() const saltNonce = useMemo(() => Date.now(), []) - const [_, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY, undefined) + const [_, setPendingSafe] = usePendingCreation() const { maxFeePerGas, maxPriorityFeePerGas } = useGasPrice() diff --git a/src/components/create-safe/usePendingCreation.ts b/src/components/create-safe/usePendingCreation.ts new file mode 100644 index 0000000000..759d5e872c --- /dev/null +++ b/src/components/create-safe/usePendingCreation.ts @@ -0,0 +1,10 @@ +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import type { PendingSafeData } from './types' + +const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' + +const usePendingCreation = () => { + return useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) +} + +export default usePendingCreation diff --git a/src/components/create-safe/useSetCreationStep.ts b/src/components/create-safe/useSetCreationStep.ts index f17b116bc4..f8ed889052 100644 --- a/src/components/create-safe/useSetCreationStep.ts +++ b/src/components/create-safe/useSetCreationStep.ts @@ -1,13 +1,11 @@ +import { useEffect } from 'react' import type { StepRenderProps } from '@/components/tx/TxStepper/useTxStepper' import useIsWrongChain from '@/hooks/useIsWrongChain' import useWallet from '@/hooks/wallets/useWallet' -import { useEffect } from 'react' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/create-safe/status/CreationStatus' -import type { PendingSafeData } from '@/components/create-safe/types.d' +import usePendingCreation from './usePendingCreation' const useSetCreationStep = (setStep: StepRenderProps['setStep']) => { - const [pendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY, undefined) + const [pendingSafe] = usePendingCreation() const wallet = useWallet() const isWrongChain = useIsWrongChain() diff --git a/yarn.lock b/yarn.lock index 16bc0449d8..866de0ad80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2463,13 +2463,20 @@ resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-modules-deployments/-/safe-modules-deployments-1.0.0.tgz#b4cfcf7782c3fa1491b84405ac06cfa4f7eff365" integrity sha512-mZkGtfGmEym7l5w2K6d/mIaS8sDmy50uGWAfTj5Q/+Y1cU9fHYU6hzhb0QaeyNM7F7HM3jg5ZTJjartZJUqu3w== -"@gnosis.pm/safe-react-gateway-sdk@^3.1.3", "@gnosis.pm/safe-react-gateway-sdk@^3.4.1": +"@gnosis.pm/safe-react-gateway-sdk@^3.1.3": version "3.4.1" resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-gateway-sdk/-/safe-react-gateway-sdk-3.4.1.tgz#6864abdb28ce0487d64929dee0205e03a49e864f" integrity sha512-b8i0BLEP9CC7S2N7hYDX3OXRYIb9asOMVW0QWh4jzsgT5xitUOPy4jrZL9kAmx8QgcMPYbD+Jjsq/R9ltzSmew== dependencies: cross-fetch "^3.1.5" +"@gnosis.pm/safe-react-gateway-sdk@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-gateway-sdk/-/safe-react-gateway-sdk-3.4.3.tgz#4551f75ee39a690f106804a5e19c9c59e2ba1ea1" + integrity sha512-KCyM+tl0Rw+Mv+N/yqNzCX3qm80WYhqPpJ3/vcV3jfxqvbBWsXN3z0TMOrx2oJb9621su8NiQmUaQAnLzv9SaQ== + dependencies: + cross-fetch "^3.1.5" + "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" From 7b46ff6b2184a5f3aa4623dcab224ac3049cb2e2 Mon Sep 17 00:00:00 2001 From: katspaugh Date: Mon, 31 Oct 2022 10:09:42 +0100 Subject: [PATCH 14/14] Rm redundant code --- src/components/create-safe/status/CreationStatus.tsx | 2 -- src/services/local-storage/useLocalStorage.ts | 8 -------- 2 files changed, 10 deletions(-) diff --git a/src/components/create-safe/status/CreationStatus.tsx b/src/components/create-safe/status/CreationStatus.tsx index 9b45884e20..86734be037 100644 --- a/src/components/create-safe/status/CreationStatus.tsx +++ b/src/components/create-safe/status/CreationStatus.tsx @@ -18,8 +18,6 @@ import useIsWrongChain from '@/hooks/useIsWrongChain' import useStatus from '@/components/create-safe/status/useStatus' import usePendingCreation from '@/components/create-safe/usePendingCreation' -export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' - type Props = { params: PendingSafeData onSubmit: StepRenderProps['onSubmit'] diff --git a/src/services/local-storage/useLocalStorage.ts b/src/services/local-storage/useLocalStorage.ts index 2856aa5eeb..9005ec1991 100644 --- a/src/services/local-storage/useLocalStorage.ts +++ b/src/services/local-storage/useLocalStorage.ts @@ -35,14 +35,6 @@ const useLocalStorage = (key: string): [Undefinable, Setter] => { [key], ) - // On mount, sync the cache with the local storage - useEffect(() => { - const lsValue = local.getItem(key) - if (lsValue !== null) { - setCache(lsValue) - } - }, [key]) - // Subscribe to changes in local storage and update the cache // This will work across tabs useEffect(() => {