From 52693cee5568610fba94c792e6911ce481d8366b Mon Sep 17 00:00:00 2001 From: "moxey.eth" Date: Thu, 12 Oct 2023 07:14:57 +1100 Subject: [PATCH] refactor: account tokens --- src/hooks/useAccountTokens.ts | 81 +++++++----- src/hooks/useSyncExternalStoreWithTracked.ts | 15 +++ src/screens/account-details.tsx | 28 ++-- src/zustand/tokens.ts | 129 ++++++++++++------- 4 files changed, 162 insertions(+), 91 deletions(-) diff --git a/src/hooks/useAccountTokens.ts b/src/hooks/useAccountTokens.ts index 224cc7c..cf5d5ac 100644 --- a/src/hooks/useAccountTokens.ts +++ b/src/hooks/useAccountTokens.ts @@ -7,7 +7,7 @@ import { createQueryKey } from '~/react-query' import type { Client } from '~/viem' import { useNetworkStore, useTokensStore } from '~/zustand' -import { getTokensKey } from '~/zustand/tokens' +import { type TokensActions, getTokensKey } from '~/zustand/tokens' import { useClient } from './useClient' type UseAccountTokensParameters = { @@ -36,60 +36,83 @@ export function useAccountTokensQueryOptions(args: UseAccountTokensParameters) { fromBlock: network.forkBlockNumber, toBlock: 'latest', }) - syncTokens({ - accountAddress: address, - tokenAddresses: tokens, - rpcUrl: network.rpcUrl, - }) + syncTokens( + { + accountAddress: address, + rpcUrl: network.rpcUrl, + }, + { + tokenAddresses: tokens, + }, + ) return tokens }, }) } -export function useAccountTokens(args: UseAccountTokensParameters) { - const queryOptions = useAccountTokensQueryOptions(args) +export function useAccountTokens({ address }: UseAccountTokensParameters) { + const queryOptions = useAccountTokensQueryOptions({ address }) const tokensStore = useTokensStore() const { network } = useNetworkStore() const addToken = useCallback( - (address: Address) => - args.address - ? tokensStore.addToken({ - accountAddress: args.address, - rpcUrl: network.rpcUrl, - tokenAddress: address, - }) + (args: Parameters[1]) => + address + ? tokensStore.addToken( + { + accountAddress: address, + rpcUrl: network.rpcUrl, + }, + args, + ) : undefined, - [args.address, network.rpcUrl, tokensStore.addToken], + [address, network.rpcUrl, tokensStore.addToken], ) + + const hideToken = useCallback( + (args: Parameters[1]) => + address + ? tokensStore.hideToken( + { + accountAddress: address, + rpcUrl: network.rpcUrl, + }, + args, + ) + : undefined, + [address, network.rpcUrl, tokensStore.hideToken], + ) + const removeToken = useCallback( - (address: Address) => - args.address - ? tokensStore.removeToken({ - accountAddress: args.address, - rpcUrl: network.rpcUrl, - tokenAddress: address, - }) + (args: Parameters[1]) => + address + ? tokensStore.removeToken( + { + accountAddress: address, + rpcUrl: network.rpcUrl, + }, + args, + ) : undefined, - [args.address, network.rpcUrl, tokensStore.removeToken], + [address, network.rpcUrl, tokensStore.removeToken], ) + const tokens = useMemo( () => - args.address + address ? tokensStore.tokens[ getTokensKey({ - accountAddress: args.address, + accountAddress: address, rpcUrl: network.rpcUrl, }) ] - ?.map((token) => (!token.removed ? token.address : undefined)) - .filter(Boolean) : [], - [args.address, network.rpcUrl, tokensStore.tokens], + [address, network.rpcUrl, tokensStore.tokens], ) return Object.assign(useQuery(queryOptions), { addToken, + hideToken, removeToken, tokens, }) diff --git a/src/hooks/useSyncExternalStoreWithTracked.ts b/src/hooks/useSyncExternalStoreWithTracked.ts index 0f96540..c4d7157 100644 --- a/src/hooks/useSyncExternalStoreWithTracked.ts +++ b/src/hooks/useSyncExternalStoreWithTracked.ts @@ -20,6 +20,8 @@ export function useSyncExternalStoreWithTracked( }: UseSyncExternalStoreWithTrackedOptions = {}, ) { const trackedKeys = useRef([]) + const tempTrackedKeys = useRef([]) + const result = useSyncExternalStoreWithSelector( subscribe, getSnapshot, @@ -44,6 +46,11 @@ export function useSyncExternalStoreWithTracked( trackedResult, Object.entries(trackedResult as { [key: string]: any }).reduce( (res, [key, value]) => { + if (isPlainObject(value) && Object.keys(value).length === 0) { + trackedKeys.current.push(key) + tempTrackedKeys.current.push(key) + } + return { ...res, [key]: { @@ -51,6 +58,14 @@ export function useSyncExternalStoreWithTracked( enumerable: true, get: () => { const newKey = `${prefix ? `${prefix}.` : ''}${key}` + + if (tempTrackedKeys.current.length > 0) + tempTrackedKeys.current.forEach( + (key) => + (tempTrackedKeys.current = + tempTrackedKeys.current.filter((x) => x !== key)), + ) + if (isPlainObject(value)) return track(value, newKey) if (trackedKeys.current.includes(newKey)) return value diff --git a/src/screens/account-details.tsx b/src/screens/account-details.tsx index 516175b..622afd6 100644 --- a/src/screens/account-details.tsx +++ b/src/screens/account-details.tsx @@ -117,13 +117,15 @@ function Tokens({ accountAddress }: { accountAddress: Address }) { {/* TODO: Handle empty state. */} - {tokens?.map((tokenAddress) => ( - - ))} + {tokens?.map((token) => + token.visible ? ( + + ) : null, + )} ) } @@ -145,7 +147,7 @@ function ImportToken({ accountAddress }: { accountAddress: Address }) { return } - addToken(address) + addToken({ tokenAddress: address }) } finally { reset() } @@ -173,7 +175,9 @@ function TokenRow({ accountAddress, tokenAddress, }: { accountAddress: Address; tokenAddress: Address }) { - const { removeToken } = useAccountTokens({ address: accountAddress }) + const { hideToken, removeToken } = useAccountTokens({ + address: accountAddress, + }) const { data: balance, error: balanceError } = useErc20Balance({ address: accountAddress, @@ -187,14 +191,14 @@ function TokenRow({ useEffect(() => { if (balanceError) { toast.error((balanceError as BaseError).shortMessage) - removeToken(tokenAddress) + removeToken({ tokenAddress }) } }, [balanceError, tokenAddress, removeToken]) useEffect(() => { if (metadataError) { toast.error((metadataError as BaseError).shortMessage) - removeToken(tokenAddress) + removeToken({ tokenAddress }) } }, [metadataError, tokenAddress, removeToken]) @@ -269,7 +273,7 @@ function TokenRow({ variant="ghost red" onClick={(e) => { e.stopPropagation() - removeToken(tokenAddress) + hideToken({ tokenAddress }) }} /> diff --git a/src/zustand/tokens.ts b/src/zustand/tokens.ts index aca2bc7..b5f9f29 100644 --- a/src/zustand/tokens.ts +++ b/src/zustand/tokens.ts @@ -3,56 +3,66 @@ import type { Address } from 'viem' import { useSyncExternalStoreWithTracked } from '~/hooks/useSyncExternalStoreWithTracked' import { createStore } from './utils' -type Token = { +export type Token = { address: Address - removed: boolean + visible: boolean } -type TokensKey = string +export type TokensKey = { + accountAddress: Address + rpcUrl: string +} +export type SerializedTokensKey = string export type TokensState = { - tokens: Record + tokens: Record } + export type TokensActions = { - addToken: (parameters: { - accountAddress: Address - tokenAddress: Address - rpcUrl: string - }) => void - removeToken: (parameters: { - accountAddress: Address - tokenAddress: Address - rpcUrl: string - }) => void - syncTokens: (parameters: { - accountAddress: Address - tokenAddresses: Address[] - rpcUrl: string - }) => void + addToken: ( + key: TokensKey, + parameters: { + tokenAddress: Address + }, + ) => void + hideToken: ( + key: TokensKey, + parameters: { + tokenAddress: Address + }, + ) => void + removeToken: ( + key: TokensKey, + parameters: { + tokenAddress: Address + }, + ) => void + syncTokens: ( + key: TokensKey, + parameters: { + tokenAddresses: Address[] + }, + ) => void } export type TokensStore = TokensState & TokensActions -export function getTokensKey(args: { - accountAddress: Address - rpcUrl: string -}): TokensKey { - const { accountAddress, rpcUrl } = args - return `${rpcUrl}-${accountAddress}`.replace(/\./g, '-') +export function getTokensKey(key: TokensKey): SerializedTokensKey { + return `${key.accountAddress}-${key.rpcUrl}`.replace(/\./g, '-') } export const tokensStore = createStore( (set) => ({ tokens: {}, - addToken(args) { - const { accountAddress, tokenAddress, rpcUrl } = args - const key = getTokensKey({ accountAddress, rpcUrl }) + addToken(key, args) { + const { tokenAddress } = args + const serializedKey = getTokensKey(key) set((state) => { const tokens = { ...state.tokens } - tokens[key] = uniqBy( + tokens[serializedKey] = uniqBy( [ - { address: tokenAddress, removed: false }, - ...(state.tokens[key] || []), + { address: tokenAddress, visible: true }, + ...(state.tokens[serializedKey] || []), ], (x) => x.address, ) @@ -63,47 +73,66 @@ export const tokensStore = createStore( } }) }, - removeToken(args) { - const { accountAddress, tokenAddress, rpcUrl } = args - const key = getTokensKey({ accountAddress, rpcUrl }) + hideToken(key, args) { + const { tokenAddress } = args + const serializedKey = getTokensKey(key) + + set((state) => { + const tokens = { ...state.tokens } + tokens[serializedKey] = (state.tokens[serializedKey] || []).map( + (token) => { + if (token.address === tokenAddress) + return { + ...token, + visible: false, + } + return token + }, + ) + return { + ...state, + tokens, + } + }) + }, + removeToken(key, args) { + const { tokenAddress } = args + const serializedKey = getTokensKey(key) set((state) => { const tokens = { ...state.tokens } - tokens[key] = (state.tokens[key] || []).map((token) => { - if (token.address === tokenAddress) - return { - ...token, - removed: true, - } - return token - }) + tokens[serializedKey] = (state.tokens[serializedKey] || []).filter( + (token) => token.address !== tokenAddress, + ) return { ...state, tokens, } }) }, - syncTokens(args) { - const { accountAddress, tokenAddresses, rpcUrl } = args - const key = getTokensKey({ accountAddress, rpcUrl }) + syncTokens(key, args) { + const { tokenAddresses } = args + const serializedKey = getTokensKey(key) set((state) => { const tokens = { ...state.tokens } for (const tokenAddress of tokenAddresses) { - const exists = (tokens[key] || []).some( + const exists = (tokens[serializedKey] || []).some( (x) => x.address === tokenAddress, ) if (!exists) - tokens[key] = [ - ...(tokens[key] || []), + tokens[serializedKey] = [ + ...(tokens[serializedKey] || []), { address: tokenAddress, - removed: false, + visible: true, }, ] } + console.log(tokens) + return { ...state, tokens, @@ -142,7 +171,7 @@ function migrate(persistedState: unknown, version: number): TokensStore { address, tokenAddresses.map((tokenAddress) => ({ address: tokenAddress, - removed: false, + visible: false, })), ]), ),