# Frontend Debugging Module Language-specific debugging guide for frontend projects (Vue 3, React, browser-based SPAs). Use alongside the main [Debugging Protocol](../SKILL.md). --- ## Toolchain Reference | Tool | Purpose | When to Use | |---|---|---| | Chrome DevTools — **Console** | Runtime errors, warnings, `console.debug` traces | **Always first** — fastest signal | | Chrome DevTools — **Network** | Request waterfall, response status, payload shape | Data not rendering, 401/403/500, CORS | | Chrome DevTools — **Elements** | Computed styles, DOM structure, CSS specificity | Styling / layout issues | | Chrome DevTools — **Performance** | Frame rate, scripting time, rendering phases | Jank, slow interactions | | Chrome DevTools — **Application** | localStorage, sessionStorage, cookies, service workers | Auth state, cached flags, stale SW | | Vue DevTools (browser extension) | Component tree, Pinia state, router history, event timeline | Vue-specific state/lifecycle issues | | `router.afterEach` / `beforeEach` logging | Navigation guard execution trace | Routing bugs, blank screens, redirect loops | | `onMounted` / `useEffect` logging | Component lifecycle confirmation | Component "never mounts" symptoms | | `vite dev --force` | Force-clear Vite module cache | Stale build artifacts after dependency changes | | `npx vue-tsc -b` | Full template type checking | Type errors that only surface at build time | --- ## Phase 1: Initialize Session — Frontend Context Add these fields to the debugging session document: ```markdown ### Frontend Context - **Framework version:** (e.g. Vue 3.5.x, React 19.x) - **Bundler:** (e.g. Vite 7.x) - **CSS framework:** (e.g. Tailwind v4, none) - **State management:** (e.g. Pinia 3.x, Redux, Zustand) - **Router:** (e.g. Vue Router 5.x, React Router 7.x) - **Browser:** (exact version — behavior varies across engines) - **PWA/Service Worker:** yes / no (stale caches are a common red herring) - **Auth provider:** (e.g. Supabase Auth, Firebase Auth, custom JWT) ``` --- ## Phase 3: Hypothesis Categories Common frontend-specific hypothesis categories: | Category | Example Hypotheses | |---|---| | **Routing & Navigation** | Guard blocks route; redirect loop between auth/onboarding; `router.replace()` resolves but component never mounts; stale `meta` on route definition | | **State Management** | Pinia store holds stale data after auth change; double-fetch from co-mounted components; reactive loss from `const { x } = store` without `storeToRefs`; store initialized before auth resolves | | **CSS × Animation** | `transitionend` event never fires due to `@layer` cascade; `mode="out-in"` blocks entering component indefinitely; animation stuck in leaving state; blank screen with zero console errors | | **Data & Rendering** | Backend payload shape mismatch (missing field → `undefined` → silent `v-if` failure); wrong reactive access pattern (`v-if="fn()"` instead of `computed`); component renders before async data resolves | | **Auth & Session** | Token expired but UI doesn't redirect; `localStorage` holds stale session after logout; race between auth state resolution and guard evaluation; middleware applies to wrong routes | --- ## Phase 4: Validation Task Patterns ### Routing & Navigation — Blank Screen After Navigation **Symptom:** User navigates, router reports success, destination component never renders. Page is blank with zero console errors. **Validation steps:** 1. **Confirm no component mount** — add lifecycle logging to the destination: ```javascript // Vue 3 onMounted(() => console.debug('[TargetView] onMounted fired')); // React useEffect(() => console.debug('[TargetView] mounted'), []); ``` If the mount log never fires but the router reports navigation complete → this is NOT a routing logic bug. 2. **Trace the guard chain** — add diagnostic logging: ```typescript router.beforeEach((to, from) => { console.debug(`[Router] beforeEach: ${from.path} → ${to.path}`); }); router.afterEach((to) => { console.debug(`[Router] afterEach: arrived at ${to.path}`); }); ``` If `afterEach` fires but `onMounted` does not → suspect the **transition mechanism** (see CSS × Animation below). 3. **Check for redirect loops** — watch the URL bar or router logs for oscillating paths (e.g. `/login` → `/home` → `/onboarding` → `/login`). 4. **Verify auth state at guard time** — log the auth store's state inside each guard to confirm it's resolved (not still loading). --- ### State Management — Stale or Missing Data **Symptom:** Component renders but shows stale data, or renders empty despite successful API call. **Validation steps:** 1. **Inspect Pinia state via Vue DevTools** — check the store's current values. Is the data present? 2. **Check for destructuring reactivity loss:** ```typescript // ❌ Loses reactivity — x is a plain value snapshot const { tasks } = useTaskStore(); // ✅ Preserves reactivity const { tasks } = storeToRefs(useTaskStore()); ``` 3. **Check for race conditions with auth:** ```typescript // The store action fires before auth resolves → API returns 401 → store stays empty // Fix: await auth resolution before first store fetch ``` 4. **Check for duplicate fetches from co-mounted components:** - Open Network tab → filter by XHR → navigate to the page - If two identical requests appear → the store needs a loading-state guard --- ### CSS × Animation — Transition Stuck (Blank Screen) > **This section subsumes the former `css-transition-pitfalls` skill.** **Symptom:** Component "never mounts" after a route change. Blank screen with zero console errors. Works on hard refresh but recurs on SPA navigation. `router.afterEach` fires but `onMounted` does not. #### The Anti-Pattern ``` User clicks link → Router navigates → Old component should leave → New component should enter ↓ CSS framework's @layer rules override transition properties ↓ transitionend event NEVER fires ↓ SPA framework waits forever ↓ New component NEVER mounts ↓ BLANK SCREEN (no errors) ``` #### Why It's Hard to Catch 1. **No errors** — everything completes from the router's perspective 2. **Works on refresh** — hard refresh bypasses the SPA transition entirely 3. **Looks like a logic bug** — the natural instinct is to debug auth state, guards, or template conditionals 4. **Intermittent** — may only trigger on specific route pairs where the leaving component has conflicting animations #### Affected SPA Frameworks | SPA Framework | Transition Mechanism | Vulnerable Mode | |---|---|---| | **Vue 3** | `` | `mode="out-in"` blocks enter until leave completes | | **React** (Framer Motion) | `AnimatePresence mode="wait"` | `mode="wait"` blocks enter until exit completes | | **React** (React Transition Group) | `SwitchTransition mode="out-in"` | Same pattern | | **Svelte** | `transition:` directive with `|local` | Less common but possible | #### CSS Frameworks That Can Cause This | CSS Framework | Why | |---|---| | **Tailwind v4** | Uses `@layer theme, base, components, utilities` — unlayered styles (like Vue's transition classes) can be unexpectedly deprioritized | | **Open Props** | Uses `@layer` for custom property organization | | **Any framework using `@layer`** | The CSS Cascade spec gives layered styles lower priority than unlayered, BUT framework bundlers may re-layer your styles | #### Validation Steps 1. **Confirm the symptom class** — add `onMounted` logging to the destination component. If mount log never fires but `afterEach` does → transition stuck. 2. **Rule out logic issues (time-box: 15 minutes):** - Navigation guard allows the route (check `afterEach` logging) - Auth state is correct at navigation time - No redirect loops 3. **Inspect the transition mechanism** — check your root layout for sequential transition modes: ```html ``` If found → this is almost certainly the cause. 4. **Verify CSS layer interference:** ```bash # Check for @layer in built CSS grep -r "@layer" node_modules/tailwindcss/ | head -5 grep -r "@import.*tailwindcss" src/ # In browser DevTools: inspect the leaving element during transition # Look for: computed opacity, computed transition property ``` 5. **Apply the fix — remove sequential transition mode:** ```html ``` Update CSS to handle simultaneous leave/enter: ```css .fade-enter-active { transition: opacity 0.15s ease-in !important; } .fade-leave-active { transition: opacity 0.15s ease-out !important; position: absolute !important; width: 100% !important; top: 0 !important; left: 0 !important; } .fade-enter-from, .fade-leave-to { opacity: 0 !important; } /* Parent needs position:relative to contain the absolute element */ #main-content { position: relative; } ``` > **Why `!important`?** CSS `@layer` cascade can override your transition classes. `!important` guarantees your transition properties apply regardless of layer ordering. #### Key Insight > **When a component doesn't mount after navigation: suspect the transition mechanism FIRST, not the routing logic.** The router will report success even when the component never renders. This is counter-intuitive because the blank screen looks like a routing/auth problem, but it's actually a CSS/animation problem. --- ### Data & Rendering — Silent Template Failures **Symptom:** Page renders but expected content is missing. No console errors. Data is present in the store. **Validation steps:** 1. **Check for `v-if` on optional properties:** ```html
``` 2. **Check for plain function calls in templates:** ```html
``` 3. **Check for backend payload mismatches** — log the raw API response and compare field names/types against what the component expects. Common: `snake_case` from backend vs `camelCase` in frontend. 4. **Check for async rendering issues:** ```html ``` --- ### Auth & Session — Redirect Loops and Ghost Sessions **Symptom:** User gets stuck in a redirect loop between login, onboarding, and home pages. Or: user appears logged out despite having valid credentials. **Validation steps:** 1. **Check localStorage for stale auth tokens:** ```javascript // In console Object.keys(localStorage).filter(k => k.includes('auth') || k.includes('supabase') || k.includes('token')); ``` 2. **Verify auth state is resolved before guards run:** ```typescript router.beforeEach(async (to) => { console.debug('[Guard] authStore.isAuthenticated:', authStore.isAuthenticated); console.debug('[Guard] authStore.isLoading:', authStore.isLoading); // If isLoading is true here, the guard is racing the auth resolution }); ``` 3. **Check for circular guard logic:** - Guard A: "if not authenticated → redirect to /login" - Guard B: "if authenticated and not onboarded → redirect to /onboarding" - Guard C: "if on /onboarding and already onboarded → redirect to /home" - If the onboarding flag is stale or the database read is async → loop 4. **Test by clearing all storage and hard-refreshing:** ```javascript localStorage.clear(); sessionStorage.clear(); location.reload(); ``` If the issue resolves → the problem is stale cached state, not logic. --- ## Phase 6: Confidence Adjustments | Evidence | Confidence Impact | |---|---| | `onMounted` log never fires but `afterEach` does | **High** — definitively a transition/rendering issue, not routing | | DevTools Network shows no request fired | **High** — data never fetched (guard blocked, or fetch never called) | | Vue DevTools shows correct Pinia state but UI is empty | **High** — template rendering bug (wrong `v-if`, missing reactivity) | | Issue resolves on hard refresh but recurs on SPA nav | **High** — transition-stuck, service worker cache, or stale SPA state | | `@layer` found in CSS framework and `mode="out-in"` is used | **Definitive** for transition-stuck diagnosis | | Issue only appears on specific route pairs | **Medium-High** — likely CSS/animation interaction specific to the leaving component | | Console shows no errors at all | **Medium** — rules out JS exceptions; narrows to CSS/template/state issues | --- ## Quick Reference: Symptom → First Action | Symptom | First Action | |---|---| | Blank screen after navigation (no errors) | Add `onMounted` logging → if never fires, check `` | | Blank screen after navigation (console errors) | Read the error — usually a runtime exception in the entering component | | Redirect loop (URL oscillates) | Log all guards with `beforeEach` → trace the loop pattern | | Data present in store but not rendered | Check `v-if` conditions for undefined/null access; check for destructuring without `storeToRefs` | | Component renders then immediately disappears | Check for a `watch` or `watchEffect` that re-triggers a navigation or state reset | | API call fires twice | Check for co-mounted components both calling the same store action; add loading-state guard | | Works locally but not in production | Check service worker cache; run `vite build` locally to reproduce; check env variables | | Works on refresh but fails on SPA navigation | Suspect transition mechanism or stale component state from previous route | | Auth state incorrect in guard | Log auth store state inside the guard; check if auth initialization is awaited before router starts | | Styles not applying | Check CSS specificity vs `@layer` cascade; inspect computed styles in DevTools Elements tab | --- ## Related Principles - Vue Idioms and Patterns @vue-idioms-and-patterns.md - TypeScript Idioms and Patterns @typescript-idioms-and-patterns.md - Error Handling Principles @error-handling-principles.md - Logging and Observability Principles @logging-and-observability-principles.md - Testing Strategy @testing-strategy.md