name: debug-stuck-ui-tabs
description: Diagnose UI tabs/sections that snap back to a default on every click — usually a render-time state coercion overwriting user input.
---
# Debug Stuck UI Tabs
## When to use
- User reports "tabs aren't working", "clicking does nothing", or "it always goes back to X"
- A view has both sidebar navigation and inner tab/section state that derive from the same source
- Clicking changes state momentarily but the next render reverts it
- Any "controlled" UI state that mysteriously resets after user interaction

## How to apply
1. Find the render entry point for the view and trace how the active tab/section is resolved each render. Look for any line that *writes* to the state (e.g., `slot.section = forSection`, `state.tab = computed`) inside a function called on every render.
2. Identify the "forcing" input — usually something derived from a parent state (route, sidebar selection, parent tab) that's recomputed each render. If a default-mapping function like `sectionForLegacyTab(state.tab)` always returns the same value when nothing has changed, that's your culprit.
3. Confirm the bug: add a temporary `console.log` of the force-input vs the user-selected value at the top of the render. If the force-input keeps overwriting the user value on every render, you've found it.
4. Fix with a "last applied" guard: add a `_lastForcedX` field to the state. Only apply the coercion when `force !== _lastForcedX` (i.e., the upstream actually changed), then update `_lastForcedX`. This distinguishes a true navigation event from an idempotent re-render.
5. Verify by clicking each tab, triggering an unrelated re-render (resize, data refresh), and confirming the selection persists.
6. Audit the same file for sibling state fields with the same anti-pattern — if one render-time coercion existed, others often do too.

## Rules
- Never write to user-controlled state inside a function that runs on every render without comparing against the previous "forcing" value.
- Never use `if (a !== b) a = b` at render time as a "sync" mechanism without tracking what triggered the sync — it will fight every user interaction.
- Always store a `_lastApplied*` / `_lastForced*` sibling field when one state value must sometimes mirror another but also accept user overrides.
- Always reproduce the bug with a console.log before changing code — the symptom ("tabs don't work") rarely names the offending line.
- Always check sibling renderers/state fields in the same file once you find one render-time coercion bug; the pattern is usually copy-pasted.