From 5a2022c2bb575bd598c8a47b3e6cd6953274b5c1 Mon Sep 17 00:00:00 2001 From: Chidimj <165854539+Chidimj@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:56:56 +0100 Subject: [PATCH] feat: add useDebounce hook, integration tests, and state management docs --- docs/state-management.md | 69 +++++++++ src/components/common/TransactionHistory.tsx | 23 ++- .../__tests__/TransactionHistory.test.tsx | 54 +++++++ src/hooks/__tests__/useDebounce.test.tsx | 77 ++++++++++ src/hooks/useDebounce.ts | 12 ++ src/pages/LandingPage.tsx | 9 +- ...gPage.debouncedSearch.integration.test.tsx | 141 ++++++++++++++++++ 7 files changed, 375 insertions(+), 10 deletions(-) create mode 100644 docs/state-management.md create mode 100644 src/components/common/__tests__/TransactionHistory.test.tsx create mode 100644 src/hooks/__tests__/useDebounce.test.tsx create mode 100644 src/hooks/useDebounce.ts create mode 100644 src/pages/__tests__/LandingPage.debouncedSearch.integration.test.tsx diff --git a/docs/state-management.md b/docs/state-management.md new file mode 100644 index 0000000..24fa7c6 --- /dev/null +++ b/docs/state-management.md @@ -0,0 +1,69 @@ +# Client State Management + +## The Rule + +| Data type | Where it lives | +| ----------------------------------------------------------------------- | ---------------------------------------- | +| Server data (creators, holdings, activity feed) | React Query (`useQuery` / `useMutation`) | +| Ephemeral UI state (modals, input values, selected tabs, loading flags) | Local `useState` | + +If the value came from an API response and needs to survive a component unmount or be shared across routes, put it in React Query. If it only controls what the user sees right now and can be re-derived on re-mount, use `useState`. + +## Query Invalidation vs Manual Refetch + +**Invalidate** after a mutation that changes server data: + +```ts +const queryClient = useQueryClient(); +queryClient.invalidateQueries({ queryKey: queryKeys.creators.list() }); +``` + +This marks cached data stale and lets React Query refetch in the background the next time the query is observed. Use this after a buy, sell, or profile update so all subscribers see fresh data automatically. + +**Refetch manually** only when you need to force an immediate reload independent of staleness — for example, a user-triggered "Refresh" button: + +```ts +const { refetch } = useQuery({ queryKey: queryKeys.wallet.holdings(address), ... }); + +``` + +Avoid calling `refetch()` inside effects or after mutations — that bypasses cache coordination and can race with invalidation. + +## Do Not Copy Server State into Local State + +Storing a React Query result in `useState` breaks cache coherence and causes stale UI after mutations. + +### Wrong + +```tsx +function CreatorProfile({ id }: { id: string }) { + const { data } = useCreatorDetail(id); + + // Never do this — local state diverges from the cache after mutations. + const [creator, setCreator] = useState(data); + + return
{creator?.title}
; +} +``` + +### Right + +```tsx +function CreatorProfile({ id }: { id: string }) { + const { data: creator } = useCreatorDetail(id); + + // Read directly from the query result — always in sync with the cache. + return
{creator?.title}
; +} +``` + +## Ephemeral UI State Examples + +These belong in `useState`, not React Query: + +- Modal open/closed: `const [open, setOpen] = useState(false)` +- Controlled input value: `const [query, setQuery] = useState('')` +- Active tab: `const [activeTab, setActiveTab] = useState('overview')` +- Optimistic loading flag: `const [submitting, setSubmitting] = useState(false)` + +None of these values need to survive a page navigation or be shared with another component tree, so there is no reason to put them in the server-state layer. diff --git a/src/components/common/TransactionHistory.tsx b/src/components/common/TransactionHistory.tsx index d55e537..d8d835c 100644 --- a/src/components/common/TransactionHistory.tsx +++ b/src/components/common/TransactionHistory.tsx @@ -159,6 +159,7 @@ const TransactionHistory: React.FC = () => { return (
{
{tx.amount} keys - {tx.price} ETH + {tx.price} XLM {formatTimestamp(tx.timestamp)}
@@ -193,9 +194,12 @@ const TransactionHistory: React.FC = () => { {(!isCompact || isExpanded) && (
-
- {tx.type === 'buy' ? '+' : '-'} - {(tx.amount * tx.price).toFixed(4)} ETH +
+ {tx.type === 'buy' ? '-' : '+'} + {(tx.amount * tx.price).toFixed(4)} XLM
{tx.txHash} @@ -213,9 +217,12 @@ const TransactionHistory: React.FC = () => { {isCompact && !isExpanded && (
-
- {tx.type === 'buy' ? '+' : '-'} - {(tx.amount * tx.price).toFixed(4)} ETH +
+ {tx.type === 'buy' ? '-' : '+'} + {(tx.amount * tx.price).toFixed(4)} XLM