Files

343 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** | `<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` | 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
<!-- 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.
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
<!-- 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:
```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
<!-- 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">
```
2. **Check for plain function calls in templates:**
```html
<!-- ❌ Re-evaluates on every render cycle; may return inconsistent results -->
<div v-if="isVisible()">
<!-- ✅ Cached, reactive -->
<div v-if="isVisibleComputed">
```
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
<!-- 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:**
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 `<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