From 0d54163047bfe2f943e2b2f885255b2ee0ff4dcd Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 04:39:10 +0900 Subject: [PATCH 1/9] [web] Implement `usePricings` --- packages/bento-web/src/hooks/pricings.ts | 158 +++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 packages/bento-web/src/hooks/pricings.ts diff --git a/packages/bento-web/src/hooks/pricings.ts b/packages/bento-web/src/hooks/pricings.ts new file mode 100644 index 00000000..8ac090be --- /dev/null +++ b/packages/bento-web/src/hooks/pricings.ts @@ -0,0 +1,158 @@ +import { safePromiseAll } from '@bento/common'; +import { pricesFromCoinGecko } from '@bento/core'; +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { useCallback, useEffect, useRef } from 'react'; + +import { useInterval } from '@/hooks/useInterval'; + +const SECONDS = 1_000; +const CACHE_TIME = 60 * SECONDS; + +type ValueVO = [v: number, t: number]; +const buildCacheStore = (prefix: string) => ({ + get: async (key: string): Promise => { + const v = localStorage.getItem(prefix); + if (!v) { + return null; + } + return JSON.parse(v)[key] || null; + }, + set: async (key: string, value: T) => { + const v = localStorage.getItem(prefix); + const prev = !v ? {} : JSON.parse(v); + localStorage.setItem(prefix, JSON.stringify({ ...prev, [key]: value })); + }, +}); +const CacheStore = buildCacheStore('@pricings'); + +type PricingMap = Record; +export const pricingsAtom = atom({}); +export const coinGeckoIdsAtom = atom([]); + +const useCoinGeckoPrices = () => { + const isInitialFetch = useRef(true); + + const coinGeckoIds = useAtomValue(coinGeckoIdsAtom); + const [prices, setPrices] = useAtom(pricingsAtom); + + const fetchPrices = useCallback(async () => { + if (!coinGeckoIds.length) { + return; + } + + const cachedIds: string[] = []; + const cachedPrices = await safePromiseAll( + coinGeckoIds.map(async (coinGeckoId) => { + const cachedValue = await CacheStore.get(coinGeckoId).catch(() => null); + if (!cachedValue) { + return [coinGeckoId, 0]; + } + const [value, cachedAt] = cachedValue as ValueVO; + + if (!isInitialFetch.current) { + if (cachedAt >= Date.now() - CACHE_TIME) { + cachedIds.push(coinGeckoId); + return [coinGeckoId, value]; + } + } else { + isInitialFetch.current = false; + } + + return [coinGeckoId, 0]; + }), + ); + let cachedPricesObject: PricingMap | null = null; + if (cachedPrices.length > 0) { + cachedPricesObject = Object.fromEntries(cachedPrices); + setPrices(cachedPricesObject as PricingMap); + } + + const cachedIdsSet = new Set(cachedIds); + const notCachedIds = coinGeckoIds.filter((id) => !cachedIdsSet.has(id)); + + const fetchedPrices = + notCachedIds.length === 0 + ? [] + : await pricesFromCoinGecko(notCachedIds).then((prices) => + Object.entries(prices), + ); + await safePromiseAll( + fetchedPrices.map(([key, price]) => + CacheStore.set(key, [price, Date.now()]), + ), + ); + + const fetchedPricesObject = Object.fromEntries(fetchedPrices); + + setPrices({ + ...cachedPricesObject, + ...fetchedPricesObject, + }); + }, [JSON.stringify(coinGeckoIds)]); + + useEffect(() => { + fetchPrices(); + }, [fetchPrices]); + + useInterval(fetchPrices, CACHE_TIME); + + return prices; +}; + +export const PricingsProvider: React.FC = () => { + useCoinGeckoPrices(); + return null; +}; + +export const usePricings = () => { + const [prices] = useAtom(pricingsAtom); + const setCoinGeckoIds = useSetAtom(coinGeckoIdsAtom); + + const getPrice = useCallback( + (coinGeckoId: string | string[]) => { + const ids = Array.isArray(coinGeckoId) ? coinGeckoId : [coinGeckoId]; + + return ids.map((id) => { + if (id in prices) { + return prices[id]; + } + setCoinGeckoIds((prev) => (prev.includes(id) ? prev : [...prev, id])); + return 0; + }); + }, + [JSON.stringify(prices)], + ); + + return { getPrice }; +}; + +// import { NextPage } from 'next'; +// import { useState } from 'react'; + +// import { PricingsProvider, usePricings } from '@/utils/pricings'; + +// const Playground: NextPage = () => { +// const { getPrice } = usePricings(); +// const [cosmos, setCosmos] = useState(false); +// return ( +//
+// +// {JSON.stringify(getPrice(['ethereum', 'bitcoin']))} +// {cosmos ? getPrice('cosmos')[0] : null} +// +//
+// ); +// }; + +// export default Playground; From ea857fc6f5d6cbd025ba70ca1ef64b001dda1223 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:00:59 +0900 Subject: [PATCH 2/9] [web] Use `usePricings` in everywhere --- packages/bento-web/pages/_app.tsx | 3 ++ .../src/dashboard/utils/useNFTBalances.ts | 8 ++-- .../src/dashboard/utils/useWalletBalances.ts | 38 ++++--------------- packages/bento-web/src/hooks/pricings.ts | 6 ++- 4 files changed, 20 insertions(+), 35 deletions(-) diff --git a/packages/bento-web/pages/_app.tsx b/packages/bento-web/pages/_app.tsx index c46a3eca..7a8a42c7 100644 --- a/packages/bento-web/pages/_app.tsx +++ b/packages/bento-web/pages/_app.tsx @@ -16,6 +16,7 @@ import styled from 'styled-components'; import { LoadingProgress } from '@/components/LoadingProgress'; import { NavigationBar } from '@/components/NavigationBar'; +import { PricingsProvider } from '@/hooks/pricings'; import { GlobalStyle } from '@/styles/GlobalStyle'; Analytics.initialize(); @@ -65,6 +66,8 @@ const App = ({ Component, pageProps }: AppProps) => { + + { + const { getPrice } = usePricings(); const [openSeaNFTBalance, setOpenSeaNFTBalance] = useState< NFTWalletBalance[] >([]); @@ -63,8 +65,8 @@ export const useNFTBalances = ({ wallets }: Options) => { }; main(); - priceFromCoinGecko('ethereum').then(setEthereumPrice); - }, [JSON.stringify(wallets)]); + setEthereumPrice(getPrice('ethereum')); + }, [getPrice, JSON.stringify(wallets)]); useEffect(() => { const flattedAssets = Object.values(fetchedAssets) diff --git a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts index 2d2554b4..7dc54613 100644 --- a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts +++ b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts @@ -1,7 +1,8 @@ import { Wallet } from '@bento/common'; -import { pricesFromCoinGecko } from '@bento/core'; import produce from 'immer'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; + +import { usePricings } from '@/hooks/pricings'; import { KEYS_BY_NETWORK } from '@/constants/networks'; import { @@ -25,6 +26,8 @@ type Options = { }; export const useWalletBalances = ({ wallets }: Options) => { + const { getPrice } = usePricings(); + const calculatedRequests = useMemo(() => { // TODO: Clean this thing up const data: PartialRecord = @@ -67,47 +70,20 @@ export const useWalletBalances = ({ wallets }: Options) => { useInterval(refetch, 60 * 1_000); const balances = useMemo(() => result.flatMap((v) => v.data ?? []), [result]); - - const coinGeckoIds = useMemo( - () => balances.flatMap((v) => v.coinGeckoId || []), - [balances], - ); - - const [coinGeckoPricesByIds, setCoinGeckoPricesByIds] = useState< - Record - >({}); - - const fetchPrices = useCallback(() => { - if (!coinGeckoIds.length) { - return; - } - - pricesFromCoinGecko(coinGeckoIds) - .then(setCoinGeckoPricesByIds) - .catch((error) => { - console.error(error); - }); - }, [JSON.stringify(coinGeckoIds)]); - - useEffect(() => { - fetchPrices(); - }, [fetchPrices]); - useInterval(fetchPrices, 60 * 1_000); - const balancesWithPrices = useMemo( () => produce(balances, (draft) => { draft.forEach((token) => { if (typeof token.price === 'undefined') { if (!!token.coinGeckoId) { - token.price = coinGeckoPricesByIds[token.coinGeckoId] ?? 0; + token.price = getPrice(token.coinGeckoId); } else { token.price = 0; } } }); }), - [balances, JSON.stringify(coinGeckoPricesByIds)], + [balances, getPrice], ); return { diff --git a/packages/bento-web/src/hooks/pricings.ts b/packages/bento-web/src/hooks/pricings.ts index 8ac090be..dad06364 100644 --- a/packages/bento-web/src/hooks/pricings.ts +++ b/packages/bento-web/src/hooks/pricings.ts @@ -104,6 +104,10 @@ export const PricingsProvider: React.FC = () => { return null; }; +interface GetPrice { + (coinGeckoId: string): number; + (coinGeckoIds: string[]): number[]; +} export const usePricings = () => { const [prices] = useAtom(pricingsAtom); const setCoinGeckoIds = useSetAtom(coinGeckoIdsAtom); @@ -121,7 +125,7 @@ export const usePricings = () => { }); }, [JSON.stringify(prices)], - ); + ) as GetPrice; return { getPrice }; }; From f09e75114368fc8dc7be42c56f483fa7ed9f2600 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:02:23 +0900 Subject: [PATCH 3/9] Rename: `getPrice` -> `getCachedPrice`, `usePricings` -> `useCachedPricings` --- packages/bento-web/src/dashboard/utils/useNFTBalances.ts | 8 ++++---- .../bento-web/src/dashboard/utils/useWalletBalances.ts | 8 ++++---- packages/bento-web/src/hooks/pricings.ts | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts index e6b57061..341456ef 100644 --- a/packages/bento-web/src/dashboard/utils/useNFTBalances.ts +++ b/packages/bento-web/src/dashboard/utils/useNFTBalances.ts @@ -4,7 +4,7 @@ import chunk from 'lodash.chunk'; import groupBy from 'lodash.groupby'; import { useEffect, useState } from 'react'; -import { usePricings } from '@/hooks/pricings'; +import { useCachedPricings } from '@/hooks/pricings'; import { NFTWalletBalance } from '@/dashboard/types/WalletBalance'; @@ -15,7 +15,7 @@ type Options = { }; export const useNFTBalances = ({ wallets }: Options) => { - const { getPrice } = usePricings(); + const { getCachedPrice } = useCachedPricings(); const [openSeaNFTBalance, setOpenSeaNFTBalance] = useState< NFTWalletBalance[] >([]); @@ -65,8 +65,8 @@ export const useNFTBalances = ({ wallets }: Options) => { }; main(); - setEthereumPrice(getPrice('ethereum')); - }, [getPrice, JSON.stringify(wallets)]); + setEthereumPrice(getCachedPrice('ethereum')); + }, [getCachedPrice, JSON.stringify(wallets)]); useEffect(() => { const flattedAssets = Object.values(fetchedAssets) diff --git a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts index 7dc54613..8666a5cf 100644 --- a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts +++ b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts @@ -2,7 +2,7 @@ import { Wallet } from '@bento/common'; import produce from 'immer'; import { useMemo } from 'react'; -import { usePricings } from '@/hooks/pricings'; +import { useCachedPricings } from '@/hooks/pricings'; import { KEYS_BY_NETWORK } from '@/constants/networks'; import { @@ -26,7 +26,7 @@ type Options = { }; export const useWalletBalances = ({ wallets }: Options) => { - const { getPrice } = usePricings(); + const { getCachedPrice } = useCachedPricings(); const calculatedRequests = useMemo(() => { // TODO: Clean this thing up @@ -76,14 +76,14 @@ export const useWalletBalances = ({ wallets }: Options) => { draft.forEach((token) => { if (typeof token.price === 'undefined') { if (!!token.coinGeckoId) { - token.price = getPrice(token.coinGeckoId); + token.price = getCachedPrice(token.coinGeckoId); } else { token.price = 0; } } }); }), - [balances, getPrice], + [balances, getCachedPrice], ); return { diff --git a/packages/bento-web/src/hooks/pricings.ts b/packages/bento-web/src/hooks/pricings.ts index dad06364..af502dde 100644 --- a/packages/bento-web/src/hooks/pricings.ts +++ b/packages/bento-web/src/hooks/pricings.ts @@ -108,11 +108,11 @@ interface GetPrice { (coinGeckoId: string): number; (coinGeckoIds: string[]): number[]; } -export const usePricings = () => { +export const useCachedPricings = () => { const [prices] = useAtom(pricingsAtom); const setCoinGeckoIds = useSetAtom(coinGeckoIdsAtom); - const getPrice = useCallback( + const getCachedPrice = useCallback( (coinGeckoId: string | string[]) => { const ids = Array.isArray(coinGeckoId) ? coinGeckoId : [coinGeckoId]; @@ -127,7 +127,7 @@ export const usePricings = () => { [JSON.stringify(prices)], ) as GetPrice; - return { getPrice }; + return { getCachedPrice }; }; // import { NextPage } from 'next'; From 45f137b5989ff46cec96d346db621844be1ef440 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:07:43 +0900 Subject: [PATCH 4/9] Remove `isInitialFetch` check; 1m anyway --- packages/bento-web/src/hooks/pricings.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/bento-web/src/hooks/pricings.ts b/packages/bento-web/src/hooks/pricings.ts index af502dde..90f35023 100644 --- a/packages/bento-web/src/hooks/pricings.ts +++ b/packages/bento-web/src/hooks/pricings.ts @@ -1,7 +1,7 @@ import { safePromiseAll } from '@bento/common'; import { pricesFromCoinGecko } from '@bento/core'; import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect } from 'react'; import { useInterval } from '@/hooks/useInterval'; @@ -30,8 +30,6 @@ export const pricingsAtom = atom({}); export const coinGeckoIdsAtom = atom([]); const useCoinGeckoPrices = () => { - const isInitialFetch = useRef(true); - const coinGeckoIds = useAtomValue(coinGeckoIdsAtom); const [prices, setPrices] = useAtom(pricingsAtom); @@ -49,13 +47,9 @@ const useCoinGeckoPrices = () => { } const [value, cachedAt] = cachedValue as ValueVO; - if (!isInitialFetch.current) { - if (cachedAt >= Date.now() - CACHE_TIME) { - cachedIds.push(coinGeckoId); - return [coinGeckoId, value]; - } - } else { - isInitialFetch.current = false; + if (cachedAt >= Date.now() - CACHE_TIME) { + cachedIds.push(coinGeckoId); + return [coinGeckoId, value]; } return [coinGeckoId, 0]; From 93548ae0bb59d6789ed9703f957aeeda4b071ae2 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:16:40 +0900 Subject: [PATCH 5/9] Manage `getCachedPrice` as context --- packages/bento-web/pages/_app.tsx | 36 +++++----- .../src/hooks/{pricings.ts => pricings.tsx} | 70 +++++++------------ 2 files changed, 44 insertions(+), 62 deletions(-) rename packages/bento-web/src/hooks/{pricings.ts => pricings.tsx} (74%) diff --git a/packages/bento-web/pages/_app.tsx b/packages/bento-web/pages/_app.tsx index 7a8a42c7..833247d3 100644 --- a/packages/bento-web/pages/_app.tsx +++ b/packages/bento-web/pages/_app.tsx @@ -66,24 +66,24 @@ const App = ({ Component, pageProps }: AppProps) => { - - - - - - - - - - -
-
-
-
- + + + + + + + + + +
+
+
+
+ + ); }; diff --git a/packages/bento-web/src/hooks/pricings.ts b/packages/bento-web/src/hooks/pricings.tsx similarity index 74% rename from packages/bento-web/src/hooks/pricings.ts rename to packages/bento-web/src/hooks/pricings.tsx index 90f35023..16e50cf2 100644 --- a/packages/bento-web/src/hooks/pricings.ts +++ b/packages/bento-web/src/hooks/pricings.tsx @@ -1,7 +1,7 @@ import { safePromiseAll } from '@bento/common'; import { pricesFromCoinGecko } from '@bento/core'; import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useInterval } from '@/hooks/useInterval'; @@ -93,64 +93,46 @@ const useCoinGeckoPrices = () => { return prices; }; -export const PricingsProvider: React.FC = () => { - useCoinGeckoPrices(); - return null; -}; - -interface GetPrice { +interface GetCachedPrice { (coinGeckoId: string): number; (coinGeckoIds: string[]): number[]; } -export const useCachedPricings = () => { +const GetCachedPriceContext = React.createContext((( + v: string | string[], +) => (typeof v === 'string' ? 0 : [0])) as GetCachedPrice); + +export const PricingsProvider: React.FC = ({ + children, +}) => { + useCoinGeckoPrices(); + const [prices] = useAtom(pricingsAtom); const setCoinGeckoIds = useSetAtom(coinGeckoIdsAtom); const getCachedPrice = useCallback( (coinGeckoId: string | string[]) => { - const ids = Array.isArray(coinGeckoId) ? coinGeckoId : [coinGeckoId]; - - return ids.map((id) => { + const single = typeof coinGeckoId === 'string'; + const ids = single ? [coinGeckoId] : coinGeckoId; + const res = ids.map((id) => { if (id in prices) { return prices[id]; } setCoinGeckoIds((prev) => (prev.includes(id) ? prev : [...prev, id])); return 0; }); + return single ? res[0] : res; }, - [JSON.stringify(prices)], - ) as GetPrice; + [prices], + ) as GetCachedPrice; + + return ( + + {children} + + ); +}; +export const useCachedPricings = () => { + const getCachedPrice = React.useContext(GetCachedPriceContext); return { getCachedPrice }; }; - -// import { NextPage } from 'next'; -// import { useState } from 'react'; - -// import { PricingsProvider, usePricings } from '@/utils/pricings'; - -// const Playground: NextPage = () => { -// const { getPrice } = usePricings(); -// const [cosmos, setCosmos] = useState(false); -// return ( -//
-// -// {JSON.stringify(getPrice(['ethereum', 'bitcoin']))} -// {cosmos ? getPrice('cosmos')[0] : null} -// -//
-// ); -// }; - -// export default Playground; From 559e74d050533c836fb99e58f6b081b8ff392c45 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:21:28 +0900 Subject: [PATCH 6/9] Replace `useMemo` that was calling `getCachedPrice` inside to `useEffect` This was to fix `Cannot update a component while rendering a different component` warning --- .../src/dashboard/utils/useWalletBalances.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts index 8666a5cf..b48ea3c0 100644 --- a/packages/bento-web/src/dashboard/utils/useWalletBalances.ts +++ b/packages/bento-web/src/dashboard/utils/useWalletBalances.ts @@ -1,6 +1,6 @@ import { Wallet } from '@bento/common'; import produce from 'immer'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useCachedPricings } from '@/hooks/pricings'; @@ -70,21 +70,24 @@ export const useWalletBalances = ({ wallets }: Options) => { useInterval(refetch, 60 * 1_000); const balances = useMemo(() => result.flatMap((v) => v.data ?? []), [result]); - const balancesWithPrices = useMemo( - () => - produce(balances, (draft) => { - draft.forEach((token) => { - if (typeof token.price === 'undefined') { - if (!!token.coinGeckoId) { - token.price = getCachedPrice(token.coinGeckoId); - } else { - token.price = 0; - } + const [balancesWithPrices, setBalancesWithPrices] = useState< + (EVMWalletBalance | CosmosSDKWalletBalance | SolanaWalletBalance)[] + >([]); + + useEffect(() => { + const result = produce(balances, (draft) => { + draft.forEach((token) => { + if (typeof token.price === 'undefined') { + if (!!token.coinGeckoId) { + token.price = getCachedPrice(token.coinGeckoId); + } else { + token.price = 0; } - }); - }), - [balances, getCachedPrice], - ); + } + }); + }); + setBalancesWithPrices(result); + }, [balances, getCachedPrice]); return { balances: balancesWithPrices, From 8a7cfb6b77e569dc2c6ef0c010962a4344a0d07c Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:29:23 +0900 Subject: [PATCH 7/9] Remove unnessasary `setCount` before each request start --- packages/bento-web/src/dashboard/utils/useMultipleRequests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts b/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts index bdca857a..274a6774 100644 --- a/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts +++ b/packages/bento-web/src/dashboard/utils/useMultipleRequests.ts @@ -19,7 +19,6 @@ export const useMultipleRequests = ( ...responsesRef.current[requestKey], isLoading: true, }; - setCount((prev) => prev + 1); axios .get(requestKey) From cc9115d793e2c5955b6832360c87e78ca8f234b8 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 05:30:15 +0900 Subject: [PATCH 8/9] Rename: `notCachedIds` -> `uncachedIds` --- packages/bento-web/src/hooks/pricings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bento-web/src/hooks/pricings.tsx b/packages/bento-web/src/hooks/pricings.tsx index 16e50cf2..4eed1e44 100644 --- a/packages/bento-web/src/hooks/pricings.tsx +++ b/packages/bento-web/src/hooks/pricings.tsx @@ -62,12 +62,12 @@ const useCoinGeckoPrices = () => { } const cachedIdsSet = new Set(cachedIds); - const notCachedIds = coinGeckoIds.filter((id) => !cachedIdsSet.has(id)); + const uncachedIds = coinGeckoIds.filter((id) => !cachedIdsSet.has(id)); const fetchedPrices = - notCachedIds.length === 0 + uncachedIds.length === 0 ? [] - : await pricesFromCoinGecko(notCachedIds).then((prices) => + : await pricesFromCoinGecko(uncachedIds).then((prices) => Object.entries(prices), ); await safePromiseAll( From a91b934c375e58f10cac0bfc135b356cacadda78 Mon Sep 17 00:00:00 2001 From: Junho Yeo Date: Wed, 12 Oct 2022 10:09:01 +0900 Subject: [PATCH 9/9] Debounce `fetchPrices` for 2s --- packages/bento-web/src/hooks/pricings.tsx | 20 ++++++++++--- packages/bento-web/src/hooks/useLazyEffect.ts | 28 +++++++++++++++++++ packages/bento-web/src/utils/debounce.ts | 22 +++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 packages/bento-web/src/hooks/useLazyEffect.ts create mode 100644 packages/bento-web/src/utils/debounce.ts diff --git a/packages/bento-web/src/hooks/pricings.tsx b/packages/bento-web/src/hooks/pricings.tsx index 4eed1e44..3d8e852c 100644 --- a/packages/bento-web/src/hooks/pricings.tsx +++ b/packages/bento-web/src/hooks/pricings.tsx @@ -1,10 +1,14 @@ import { safePromiseAll } from '@bento/common'; import { pricesFromCoinGecko } from '@bento/core'; import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { useInterval } from '@/hooks/useInterval'; +import { Config } from '@/utils'; + +import { useLazyEffect } from './useLazyEffect'; + const SECONDS = 1_000; const CACHE_TIME = 60 * SECONDS; @@ -38,6 +42,10 @@ const useCoinGeckoPrices = () => { return; } + if (Config.ENVIRONMENT !== 'production') { + console.log('hit'); + } + const cachedIds: string[] = []; const cachedPrices = await safePromiseAll( coinGeckoIds.map(async (coinGeckoId) => { @@ -84,9 +92,13 @@ const useCoinGeckoPrices = () => { }); }, [JSON.stringify(coinGeckoIds)]); - useEffect(() => { - fetchPrices(); - }, [fetchPrices]); + useLazyEffect( + () => { + fetchPrices(); + }, + [fetchPrices], + 2_000, + ); useInterval(fetchPrices, CACHE_TIME); diff --git a/packages/bento-web/src/hooks/useLazyEffect.ts b/packages/bento-web/src/hooks/useLazyEffect.ts new file mode 100644 index 00000000..19c242c3 --- /dev/null +++ b/packages/bento-web/src/hooks/useLazyEffect.ts @@ -0,0 +1,28 @@ +import { + DependencyList, + EffectCallback, + useCallback, + useEffect, + useRef, +} from 'react'; + +import { debounce } from '@/utils/debounce'; + +export function useLazyEffect( + effect: EffectCallback, + deps: DependencyList = [], + wait: number, +) { + const cleanUp = useRef void)>(); + const effectRef = useRef(); + effectRef.current = useCallback(effect, deps); + const lazyEffect = useCallback( + debounce(() => (cleanUp.current = effectRef.current?.()), wait), + [], + ); + useEffect(lazyEffect, deps); + useEffect(() => { + return () => + cleanUp.current instanceof Function ? cleanUp.current() : undefined; + }, []); +} diff --git a/packages/bento-web/src/utils/debounce.ts b/packages/bento-web/src/utils/debounce.ts new file mode 100644 index 00000000..5ce2ee21 --- /dev/null +++ b/packages/bento-web/src/utils/debounce.ts @@ -0,0 +1,22 @@ +import { Config } from './Config'; + +export const debounce = ( + func: (...args: Arguments) => any, + wait: number, + immediate?: boolean, +) => { + let timeout: NodeJS.Timeout | null = null; + return function (...args: Arguments) { + if (timeout) { + clearTimeout(timeout); + } + if (Config.ENVIRONMENT !== 'production') { + console.log('debounce'); + } + timeout = setTimeout(function () { + timeout = null; + if (!immediate) func(...args); + }, wait); + if (immediate && !timeout) func(...args); + }; +};