From 5a121159d9e59f66e88e06db29eb52e9294357b6 Mon Sep 17 00:00:00 2001 From: Bilal Ishaq Date: Sun, 19 Jul 2026 10:30:03 +0100 Subject: [PATCH] docs: document asynchronous page component state lifecycle standard --- docs/state-management.md | 89 ++++++++++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/docs/state-management.md b/docs/state-management.md index 24fa7c6d..6dca54af 100644 --- a/docs/state-management.md +++ b/docs/state-management.md @@ -2,10 +2,10 @@ ## 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` | +| 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`. @@ -20,7 +20,9 @@ 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: +## Refetch manually + +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), ... }); @@ -37,12 +39,9 @@ Storing a React Query result in `useState` breaks cache coherence and causes sta ```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}
; + const { data } = useCreatorDetail(id); + const [creator, setCreator] = useState(data); + return
{creator?.title}
; } ``` @@ -50,20 +49,76 @@ function CreatorProfile({ id }: { id: string }) { ```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}
; + const { data: creator } = useCreatorDetail(id); + 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. +--- + +## Handling Asynchronous States (Loading, Error, Data) + +To avoid inconsistent layout shift and unhandled application crashes, every page component introducing server mutations or asynchronous fetching must explicitly handle the three lifecycle states: **Loading**, **Error**, and **Data**. + +### 1. The Three-State Pattern Flowchart +1. **Loading State:** Immediately show a structural fallback layout matching the structural scale of the destination layout. Never present empty blank states or unformatted spinning animations. +2. **Error State:** Intercept request faults gracefully. Provide an isolated contextual failure notice alongside a trigger to manually execute a `refetch()` query call. +3. **Data State:** Render layout presentation markup smoothly once the data successfully hydrates. + +### 2. Choosing a Skeleton Component +Match your structural loading fallbacks strictly to your structural data card layout sizes: +* Use `` for complete full-bleed layout views or single entity profile view roots. +* Use `` wrapped inside layout grids for multi-item entity dashboards, galleries, or listing blocks. + +### 3. Error Architecture Strategy: Boundary vs. Inline States +* **Error Boundaries (`CreatorPageErrorBoundary`):** Use at the route level to safely isolate catastrophic runtime engine failures, critical layout state breakdowns, or complete backend authorization drops across whole pages. +* **Inline Contextual States (`SectionErrorBoundary`):** Use for sub-components, standalone layout modules, tabs, or localized search bars where a remote service query issue shouldn't block a user from browsing the remainder of the active application canvas. Always supply the React Query context `refetch` callback method directly to retry controls. + +### 4. Code Implementation Blueprint + +```tsx +import React from 'react'; +import { useCreatorDetail } from '@/hooks/useCreatorDetail'; +import { CreatorSkeleton } from '@/components/common/CreatorSkeleton'; + +interface CreatorDashboardPageProps { + creatorId: string; +} + +export function CreatorDashboardPage({ creatorId }: CreatorDashboardPageProps) { + const { data: creator, isLoading, isError, error, refetch } = useCreatorDetail(creatorId); + + if (isLoading) { + return ; + } + + if (isError) { + return ( +
+

Failed to load profile details

+

+ {error instanceof Error ? error.message : 'An unexpected data layer exception occurred.'} +

+ +
+ ); + } + + return ( +
+

{creator?.name}

+

{creator?.bio}

+
+ ); +} +```