react-tab-state-persistence

/home/avalon/.hermes/skills/software-development/react-tab-state-persistence/SKILL.md · raw

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 && <Component>} 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:

// BEFORE (unmounts on tab switch):
{viewMode === "transits" && <TransitBodygraphPanel />}

// AFTER (stays mounted, state survives):
<div style={{ display: viewMode === "transits" ? "block" : "none" }}>
  <TransitBodygraphPanel />
</div>

Step 2: Add Cache-Check on Mount

Add an effect that fetches saved data on mount and when key params change:

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:

if (!readingChecked) {
  return <div className="empty-state">Checking for saved reading...</div>;
}

if (!reading) {
  return <div className="empty-state">Nothing runs until you click generate</div>;
}

Step 4: (Optional) Handle In-Progress Generations

If backend tracks "generating" status, re-subscribe to SSE stream:

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