You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PoolDetailClient bypasses the shared usePools() cache entirely — direct uncached getFactoryPools()/getPoolDepositors() calls duplicate RPC traffic and never refresh #143
src/app/farm/[poolId]/PoolDetailClient.tsx fetches its pool data and depositor list with two independent, hand-rolled useEffects calling sorobanService directly, entirely bypassing React Query:
Every other consumer of pool data in this app goes through usePools() (useSorobanQuery.ts:29-38), a React Query hook with staleTime: 30000/refetchInterval: 60000, meaning the Farm page's pool list is shared, cached, and kept fresh across all components that call usePools(). PoolDetailClient doesn't call usePools() at all — it calls sorobanService.getFactoryPools() directly, which is the exact same expensive operation usePools()'s queryFn performs (getAccount + simulateTransaction against the factory contract, per soroban.ts:705-741) — with two consequences: (1) Redundant RPC traffic. A user who visits /farm (populating usePools()'s fresh, 30-second-stale-tolerant cache) and then clicks into a pool's detail page triggers a completely fresh, uncached getFactoryPools() round trip, even though the exact same data was fetched moments earlier and is still sitting in the QueryClient's cache under the ['pools'] key — PoolDetailClient simply never looks there. (2) Permanent staleness. Because this is a one-shot useEffect fetch with no refetchInterval/staleTime mechanism at all, pool.dailyRate/pool.totalLocked/pool.totalUsers shown on the detail page never update again for the lifetime of that page view — unlike the Farm page's list, which silently refreshes every 60 seconds via usePools(). A user who leaves a pool detail page open (e.g. in a background tab, or just idle) can be looking at TVL/rate/user-count figures that are arbitrarily out of date, with no polling and no visual staleness indicator.
Requirements
Replace PoolDetailClient's direct sorobanService.getFactoryPools() call with usePools(), deriving pool via pools?.find((p) => p.id === poolId), so the detail page shares the same cache, staleTime, and refetchInterval as the Farm page's list.
If getPoolDepositors doesn't already have a React Query wrapper, add one (e.g. usePoolDepositors(poolId)) with a sensible staleTime/refetchInterval, replacing the current one-shot useEffect, so depositor data also refreshes over time rather than being frozen at mount.
Acceptance Criteria
Visiting /farm and then a pool's detail page for the same pool does not trigger a second, independent getFactoryPools() RPC round trip if the usePools() cache is still fresh (verifiable via a call-count assertion in a test with both components mounted against a shared QueryClient).
PoolDetailClient's displayed pool stats (dailyRate, totalLocked, totalUsers) update on the existing usePools() refetch interval, rather than remaining frozen for the lifetime of the page view.
getPoolDepositors is called through a React Query hook with a defined refetch policy, not a one-shot useEffect.
Existing behavior (pool-not-found error state, loading skeletons) is preserved once migrated.
Additional Notes
More precise references
src/app/farm/[poolId]/PoolDetailClient.tsx:127-148 (the getFactoryPoolsuseEffect) and :150-167 (the getPoolDepositorsuseEffect) — confirmed both call sorobanService methods directly, with no usePools/useQuery import anywhere in this file (confirmed via the file's full import list, PoolDetailClient.tsx:1-41).
src/hooks/useSorobanQuery.ts:29-38 (usePools) — confirmed the existing, shared, cached hook this page should be using instead, including its staleTime: 30000/refetchInterval: 60000 policy.
src/lib/soroban.ts:705-741 (getFactoryPools) — confirmed this is the same underlying, non-trivial getAccount + simulateTransaction operation usePools()'s queryFn calls, i.e. genuinely duplicated work, not two different code paths that happen to look similar.
src/app/farm/page.tsx:445-450 — confirmed the Farm page's own usage of usePools(), for direct comparison of the pattern PoolDetailClient should adopt.
Additional edge cases
Because pool is derived by .find((p) => p.id === poolId) against a freshly-fetched array each time in the current implementation, migrating to usePools() should preserve the same not-found handling (if (!found) setError("Pool not found."), PoolDetailClient.tsx:136) — with usePools(), this becomes a useMemo derivation from pools that recomputes if the pool disappears from a later factory poll (e.g. a pool is deactivated), which is arguably more correct than today's one-shot check, but should be called out explicitly in the PR since it's a behavior change (today, a pool that's removed from the factory after the initial page load stays displayed until the user navigates away and back; after this fix, it could disappear/error live).
Mount both FarmPage (or just usePools() directly) and PoolDetailClient under one shared QueryClient/mocked sorobanService.getFactoryPools; assert getFactoryPools is called once (cache hit for the second consumer), not twice.
Advance fake timers past usePools()'s refetchInterval; assert PoolDetailClient's displayed dailyRate/totalLocked update to reflect a changed mocked response, where today they would remain frozen.
Overview
src/app/farm/[poolId]/PoolDetailClient.tsxfetches its pool data and depositor list with two independent, hand-rolleduseEffects callingsorobanServicedirectly, entirely bypassing React Query:Every other consumer of pool data in this app goes through
usePools()(useSorobanQuery.ts:29-38), a React Query hook withstaleTime: 30000/refetchInterval: 60000, meaning the Farm page's pool list is shared, cached, and kept fresh across all components that callusePools().PoolDetailClientdoesn't callusePools()at all — it callssorobanService.getFactoryPools()directly, which is the exact same expensive operationusePools()'squeryFnperforms (getAccount+simulateTransactionagainst the factory contract, persoroban.ts:705-741) — with two consequences: (1) Redundant RPC traffic. A user who visits/farm(populatingusePools()'s fresh, 30-second-stale-tolerant cache) and then clicks into a pool's detail page triggers a completely fresh, uncachedgetFactoryPools()round trip, even though the exact same data was fetched moments earlier and is still sitting in theQueryClient's cache under the['pools']key —PoolDetailClientsimply never looks there. (2) Permanent staleness. Because this is a one-shotuseEffectfetch with norefetchInterval/staleTimemechanism at all,pool.dailyRate/pool.totalLocked/pool.totalUsersshown on the detail page never update again for the lifetime of that page view — unlike the Farm page's list, which silently refreshes every 60 seconds viausePools(). A user who leaves a pool detail page open (e.g. in a background tab, or just idle) can be looking at TVL/rate/user-count figures that are arbitrarily out of date, with no polling and no visual staleness indicator.Requirements
PoolDetailClient's directsorobanService.getFactoryPools()call withusePools(), derivingpoolviapools?.find((p) => p.id === poolId), so the detail page shares the same cache,staleTime, andrefetchIntervalas the Farm page's list.getPoolDepositorsdoesn't already have a React Query wrapper, add one (e.g.usePoolDepositors(poolId)) with a sensiblestaleTime/refetchInterval, replacing the current one-shotuseEffect, so depositor data also refreshes over time rather than being frozen at mount.Acceptance Criteria
/farmand then a pool's detail page for the same pool does not trigger a second, independentgetFactoryPools()RPC round trip if theusePools()cache is still fresh (verifiable via a call-count assertion in a test with both components mounted against a sharedQueryClient).PoolDetailClient's displayed pool stats (dailyRate,totalLocked,totalUsers) update on the existingusePools()refetch interval, rather than remaining frozen for the lifetime of the page view.getPoolDepositorsis called through a React Query hook with a defined refetch policy, not a one-shotuseEffect.Additional Notes
More precise references
src/app/farm/[poolId]/PoolDetailClient.tsx:127-148(thegetFactoryPoolsuseEffect) and:150-167(thegetPoolDepositorsuseEffect) — confirmed both callsorobanServicemethods directly, with nousePools/useQueryimport anywhere in this file (confirmed via the file's full import list,PoolDetailClient.tsx:1-41).src/hooks/useSorobanQuery.ts:29-38(usePools) — confirmed the existing, shared, cached hook this page should be using instead, including itsstaleTime: 30000/refetchInterval: 60000policy.src/lib/soroban.ts:705-741(getFactoryPools) — confirmed this is the same underlying, non-trivialgetAccount+simulateTransactionoperationusePools()'squeryFncalls, i.e. genuinely duplicated work, not two different code paths that happen to look similar.src/app/farm/page.tsx:445-450— confirmed the Farm page's own usage ofusePools(), for direct comparison of the patternPoolDetailClientshould adopt.Additional edge cases
poolis derived by.find((p) => p.id === poolId)against a freshly-fetched array each time in the current implementation, migrating tousePools()should preserve the same not-found handling (if (!found) setError("Pool not found."),PoolDetailClient.tsx:136) — withusePools(), this becomes auseMemoderivation frompoolsthat recomputes if the pool disappears from a later factory poll (e.g. a pool is deactivated), which is arguably more correct than today's one-shot check, but should be called out explicitly in the PR since it's a behavior change (today, a pool that's removed from the factory after the initial page load stays displayed until the user navigates away and back; after this fix, it could disappear/error live).getPoolDepositors(soroban.ts:1361-1420) itself has the known event-scan-truncation limitation already tracked in Event-scan features silently truncate results instead of paginating with the RPC's continuation cursor #103/Event-scan features silently truncate results instead of paginating with the RPC's continuation cursor #73 — wrapping it in a React Query hook doesn't change that underlying limitation, just the fetching/caching behavior around it; worth noting so this fix isn't mistaken for also addressing pagination completeness.Implementation sketch
Test/reproduction plan
FarmPage(or justusePools()directly) andPoolDetailClientunder one sharedQueryClient/mockedsorobanService.getFactoryPools; assertgetFactoryPoolsis called once (cache hit for the second consumer), not twice.usePools()'srefetchInterval; assertPoolDetailClient's displayeddailyRate/totalLockedupdate to reflect a changed mocked response, where today they would remain frozen.Cross-references
useAllUserPositions/useTotalUserCredits's per-pool fan-out; this issue is aboutPoolDetailClientbypassing the shared pools cache entirely via a parallel, non-React-Query fetch path that SorobanService issues a fresh simulateTransaction round trip per call with no de-duplication across concurrently-mounted components #90's proposed fixes wouldn't touch.