15 KiB
Frontend Debugging Module
Language-specific debugging guide for frontend projects (Vue 3, React, browser-based SPAs). Use alongside the main Debugging Protocol.
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:
### 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:
-
Confirm no component mount — add lifecycle logging to the destination:
// 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.
-
Trace the guard chain — add diagnostic logging:
router.beforeEach((to, from) => { console.debug(`[Router] beforeEach: ${from.path} → ${to.path}`); }); router.afterEach((to) => { console.debug(`[Router] afterEach: arrived at ${to.path}`); });If
afterEachfires butonMounteddoes not → suspect the transition mechanism (see CSS × Animation below). -
Check for redirect loops — watch the URL bar or router logs for oscillating paths (e.g.
/login→/home→/onboarding→/login). -
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:
-
Inspect Pinia state via Vue DevTools — check the store's current values. Is the data present?
-
Check for destructuring reactivity loss:
// ❌ Loses reactivity — x is a plain value snapshot const { tasks } = useTaskStore(); // ✅ Preserves reactivity const { tasks } = storeToRefs(useTaskStore()); -
Check for race conditions with auth:
// The store action fires before auth resolves → API returns 401 → store stays empty // Fix: await auth resolution before first store fetch -
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-pitfallsskill.
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
- No errors — everything completes from the router's perspective
- Works on refresh — hard refresh bypasses the SPA transition entirely
- Looks like a logic bug — the natural instinct is to debug auth state, guards, or template conditionals
- 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 | <Transition mode="out-in"> |
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` |
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
-
Confirm the symptom class — add
onMountedlogging to the destination component. If mount log never fires butafterEachdoes → transition stuck. -
Rule out logic issues (time-box: 15 minutes):
- Navigation guard allows the route (check
afterEachlogging) - Auth state is correct at navigation time
- No redirect loops
- Navigation guard allows the route (check
-
Inspect the transition mechanism — check your root layout for sequential transition modes:
<!-- Vue: look for mode="out-in" --> <Transition name="fade" mode="out-in"> <!-- React Framer Motion: look for mode="wait" --> <AnimatePresence mode="wait"> <!-- React Transition Group: look for mode="out-in" --> <SwitchTransition mode="out-in">If found → this is almost certainly the cause.
-
Verify CSS layer interference:
# 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 -
Apply the fix — remove sequential transition mode:
<!-- BEFORE (broken): sequential, depends on transitionend --> <Transition name="fade" mode="out-in"> <component :is="Component" /> </Transition> <!-- AFTER (fixed): simultaneous, always mounts new component --> <Transition name="fade"> <component :is="Component" :key="$route.path" /> </Transition>Update CSS to handle simultaneous leave/enter:
.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@layercascade can override your transition classes.!importantguarantees 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:
-
Check for
v-ifon optional properties:<!-- Silent failure if exercise.options is undefined — v-if produces no error --> <div v-if="exercise.options.length > 0"> <!-- Fix: optional chaining --> <div v-if="exercise.options?.length"> -
Check for plain function calls in templates:
<!-- ❌ Re-evaluates on every render cycle; may return inconsistent results --> <div v-if="isVisible()"> <!-- ✅ Cached, reactive --> <div v-if="isVisibleComputed"> -
Check for backend payload mismatches — log the raw API response and compare field names/types against what the component expects. Common:
snake_casefrom backend vscamelCasein frontend. -
Check for async rendering issues:
<!-- Component renders before data arrives — shows empty state permanently --> <TaskCard :task="task" /> <!-- Fix: guard with v-if --> <TaskCard v-if="task" :task="task" />
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:
-
Check localStorage for stale auth tokens:
// In console Object.keys(localStorage).filter(k => k.includes('auth') || k.includes('supabase') || k.includes('token')); -
Verify auth state is resolved before guards run:
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 }); -
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
-
Test by clearing all storage and hard-refreshing:
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 <Transition mode> |
| 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