--- name: react-tab-state-persistence description: Fix React tab components losing state on navigation - keep mounted with display:none, add cache-check on mount, handle in-progress generations trigger: When switching tabs causes component state to reset, readings disappear, or cached data doesn't auto-load --- # React Tab State Persistence Fix ## When to Use This Skill Use when a React component loses state when switching between tabs/views in a tabbed interface. Common symptoms: - Reading/generation state disappears when navigating away from a tab - "Generate" button shows even though data was just generated - SSE streams die when switching tabs - No auto-loading of cached data on return to tab ## Root Cause Patterns 1. **Conditional rendering with `&&`**: `{showTab && }` unmounts the component completely 2. **No cache-check on mount**: Component doesn't fetch saved data when it mounts 3. **Inline state that should be lifted**: Reading state lives in the tab component instead of a shared context ## Fix Steps ### Step 1: Keep Component Mounted with display:none/block Change from conditional rendering to CSS visibility: ```tsx // BEFORE (unmounts on tab switch): {viewMode === "transits" && } // AFTER (stays mounted, state survives):
``` ### Step 2: Add Cache-Check on Mount Add an effect that fetches saved data on mount and when key params change: ```tsx useEffect(() => { let cancelled = false; async function checkCache() { const keyParams = { natalFp, localDate, timeZone, dayAnchorPolicy }; try { const res = await fetch(`/api/endpoint/latest?${new URLSearchParams(keyParams)}`); if (!cancelled) { if (res.ok) { const data = await res.json(); setReading(data.reading); setSections(data.sections); setReadingChecked(true); } else { setReadingChecked(true); // No cache, show generate button } } } catch { if (!cancelled) setReadingChecked(true); } } checkCache(); return () => { cancelled = true; }; }, [natalFp, localDate, timeZone, dayAnchorPolicy]); ``` ### Step 3: Handle Loading State During Cache Check Add a `readingChecked` state (starts as `false`) and show loading UI while checking: ```tsx if (!readingChecked) { return
Checking for saved reading...
; } if (!reading) { return
Nothing runs until you click generate
; } ``` ### Step 4: (Optional) Handle In-Progress Generations If backend tracks "generating" status, re-subscribe to SSE stream: ```tsx if (savedReading?.status === "generating") { // Re-connect to /subscribe/[id] to resume streaming subscribeToGeneration(savedReading.id); } ``` ## Verification Steps 1. Generate data in the tab 2. Switch to another tab 3. Return - data should still be visible (no flash of empty state) 4. Refresh page and navigate to tab - cached data should auto-load 5. Start generation, switch away mid-stream, return - should still be streaming or show completed result ## Common Pitfalls - Forgetting to clean up async operations (use cancelled flag or AbortController) - Not including all key params in dependency array (causes stale cache checks) - Checking cache before required params are ready (add guards for falsy values) - Backend `/latest` endpoint requires exact match on model/engine/prompt - either pass them or ensure defaults match ## Related Patterns - Lift shared state to React Context for cross-tab persistence - Use `useRef` to track subscription handles for cleanup - Backend should support "resume generation" pattern with reading IDs