feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
---
|
||||
title: Per-Route beforeEnter Guards Ignore Param/Query Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: Route-level beforeEnter guards don't fire when only params, query, or hash change, causing unexpected bypasses of validation logic
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, navigation-guards, params, query]
|
||||
---
|
||||
|
||||
# Per-Route beforeEnter Guards Ignore Param/Query Changes
|
||||
|
||||
**Impact: MEDIUM** - The `beforeEnter` guard defined in route configuration only triggers when entering a route from a DIFFERENT route. Changes to params, query strings, or hash within the same route do NOT trigger `beforeEnter`, potentially bypassing important validation logic.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Use in-component `onBeforeRouteUpdate` for param/query changes
|
||||
- [ ] Or use global `beforeEach` with route.params/query checks
|
||||
- [ ] Document which guards protect which scenarios
|
||||
- [ ] Test navigation between same route with different params
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// router.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/orders/:id',
|
||||
component: OrderDetail,
|
||||
beforeEnter: async (to, from) => {
|
||||
// This runs when entering from /products
|
||||
// But NOT when navigating from /orders/1 to /orders/2!
|
||||
const order = await checkOrderAccess(to.params.id)
|
||||
if (!order.canView) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Scenario:**
|
||||
1. User navigates from `/products` to `/orders/1` - beforeEnter runs, access checked
|
||||
2. User navigates from `/orders/1` to `/orders/2` - beforeEnter DOES NOT run!
|
||||
3. User might access order they don't have permission for!
|
||||
|
||||
## What Triggers beforeEnter vs. What Doesn't
|
||||
|
||||
| Navigation | beforeEnter fires? |
|
||||
|------------|-------------------|
|
||||
| `/products` → `/orders/1` | YES |
|
||||
| `/orders/1` → `/orders/2` | NO |
|
||||
| `/orders/1` → `/orders/1?tab=details` | NO |
|
||||
| `/orders/1#section` → `/orders/1#other` | NO |
|
||||
| `/orders/1` → `/products` → `/orders/2` | YES (leaving and re-entering) |
|
||||
|
||||
## Solution 1: Add In-Component Guard
|
||||
|
||||
```vue
|
||||
<!-- OrderDetail.vue -->
|
||||
<script setup>
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
// Handle param changes within the same route
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
if (to.params.id !== from.params.id) {
|
||||
const order = await checkOrderAccess(to.params.id)
|
||||
if (!order.canView) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Solution 2: Use Global beforeEach Instead
|
||||
|
||||
```javascript
|
||||
// router.js
|
||||
router.beforeEach(async (to, from) => {
|
||||
// Handle all order access checks globally
|
||||
if (to.name === 'OrderDetail') {
|
||||
// This runs on EVERY navigation to this route, including param changes
|
||||
const order = await checkOrderAccess(to.params.id)
|
||||
if (!order.canView) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Solution 3: Combine Both Guards
|
||||
|
||||
```javascript
|
||||
// router.js - For entering from different route
|
||||
const routes = [
|
||||
{
|
||||
path: '/orders/:id',
|
||||
component: OrderDetail,
|
||||
beforeEnter: (to) => validateOrderAccess(to.params.id)
|
||||
}
|
||||
]
|
||||
|
||||
// In component - For param changes within route
|
||||
// OrderDetail.vue
|
||||
onBeforeRouteUpdate((to) => validateOrderAccess(to.params.id))
|
||||
|
||||
// Shared validation function
|
||||
async function validateOrderAccess(orderId) {
|
||||
const order = await checkOrderAccess(orderId)
|
||||
if (!order.canView) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Solution 4: Use beforeEnter with Array of Guards
|
||||
|
||||
```javascript
|
||||
// guards/orderGuards.js
|
||||
export const orderAccessGuard = async (to) => {
|
||||
const order = await checkOrderAccess(to.params.id)
|
||||
if (!order.canView) {
|
||||
return '/unauthorized'
|
||||
}
|
||||
}
|
||||
|
||||
// router.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/orders/:id',
|
||||
component: OrderDetail,
|
||||
beforeEnter: [orderAccessGuard] // Can add multiple guards
|
||||
}
|
||||
]
|
||||
|
||||
// Still need in-component guard for param changes!
|
||||
```
|
||||
|
||||
## Full Navigation Guard Execution Order
|
||||
|
||||
Understanding when each guard type fires:
|
||||
|
||||
```
|
||||
1. beforeRouteLeave (in-component, leaving component)
|
||||
2. beforeEach (global)
|
||||
3. beforeEnter (per-route, ONLY when entering from different route)
|
||||
4. beforeRouteEnter (in-component, entering component)
|
||||
5. beforeResolve (global)
|
||||
6. afterEach (global, after navigation confirmed)
|
||||
|
||||
For param/query changes on same route:
|
||||
1. beforeRouteUpdate (in-component) - ONLY this fires!
|
||||
2. beforeEach (global)
|
||||
3. beforeResolve (global)
|
||||
4. afterEach (global)
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **beforeEnter is for route ENTRY only** - Not for within-route changes
|
||||
2. **Use onBeforeRouteUpdate for param changes** - This is the in-component solution
|
||||
3. **Global beforeEach always runs** - Good for centralized validation
|
||||
4. **Test param change scenarios** - Easy to miss during development
|
||||
5. **Consider security implications** - Param-based access control needs both guards
|
||||
|
||||
## Reference
|
||||
- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
|
||||
- [Vue Router Per-Route Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard)
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
title: beforeRouteEnter Cannot Access Component Instance
|
||||
impact: MEDIUM
|
||||
impactDescription: The beforeRouteEnter guard runs before component creation, so 'this' is undefined; use the next callback to access the instance
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, navigation-guards, lifecycle, this]
|
||||
---
|
||||
|
||||
# beforeRouteEnter Cannot Access Component Instance
|
||||
|
||||
**Impact: MEDIUM** - The `beforeRouteEnter` in-component navigation guard executes BEFORE the component is created, meaning you cannot access `this` or any component instance properties. This is the ONLY navigation guard that supports a callback in the `next()` function to access the component instance after navigation.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Use next(vm => ...) callback to access component instance
|
||||
- [ ] Or use composition API guards which have different patterns
|
||||
- [ ] Move data fetching logic appropriately based on timing needs
|
||||
- [ ] Consider using global guards for data that doesn't need component access
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// Options API - WRONG: this is undefined
|
||||
export default {
|
||||
data() {
|
||||
return { user: null }
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
// BUG: this is undefined here - component doesn't exist yet!
|
||||
this.user = await fetchUser(to.params.id) // ERROR!
|
||||
next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Solution: Use next() Callback (Options API)
|
||||
|
||||
```javascript
|
||||
// Options API - CORRECT: Use callback to access vm
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: null,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
|
||||
beforeRouteEnter(to, from, next) {
|
||||
// Fetch data before component exists
|
||||
fetchUser(to.params.id)
|
||||
.then(user => {
|
||||
// Pass callback to next() - receives component instance as 'vm'
|
||||
next(vm => {
|
||||
vm.user = user
|
||||
vm.loading = false
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
next(vm => {
|
||||
vm.error = error
|
||||
vm.loading = false
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Solution: Async beforeRouteEnter (Options API)
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
data() {
|
||||
return { userData: null }
|
||||
},
|
||||
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
try {
|
||||
const user = await fetchUser(to.params.id)
|
||||
|
||||
// Still need callback for component access
|
||||
next(vm => {
|
||||
vm.userData = user
|
||||
})
|
||||
} catch (error) {
|
||||
// Redirect on error
|
||||
next('/error')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Composition API Alternative
|
||||
|
||||
In Composition API with `<script setup>`, you cannot use `beforeRouteEnter` directly because the component instance is being set up. Use different patterns instead:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const user = ref(null)
|
||||
const loading = ref(true)
|
||||
|
||||
// Option 1: Fetch in onMounted (after component exists)
|
||||
onMounted(async () => {
|
||||
user.value = await fetchUser(route.params.id)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
// Option 2: Handle subsequent param changes
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
if (to.params.id !== from.params.id) {
|
||||
loading.value = true
|
||||
user.value = await fetchUser(to.params.id)
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Route-Level Data Fetching
|
||||
|
||||
For data that should load BEFORE navigation, use route-level guards:
|
||||
|
||||
```javascript
|
||||
// router.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/users/:id',
|
||||
component: () => import('./UserProfile.vue'),
|
||||
beforeEnter: async (to, from) => {
|
||||
try {
|
||||
// Store data for component to access
|
||||
const user = await fetchUser(to.params.id)
|
||||
to.meta.user = user // Attach to route meta
|
||||
} catch (error) {
|
||||
return '/error'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- UserProfile.vue -->
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
// Access pre-fetched data from meta
|
||||
const user = route.meta.user
|
||||
</script>
|
||||
```
|
||||
|
||||
## Comparison of Navigation Guards
|
||||
|
||||
| Guard | Has `this`/component? | Can delay navigation? | Use case |
|
||||
|-------|----------------------|----------------------|----------|
|
||||
| beforeRouteEnter | NO (use next callback) | YES | Pre-fetch, redirect if data missing |
|
||||
| beforeRouteUpdate | YES | YES | React to param changes |
|
||||
| beforeRouteLeave | YES | YES | Unsaved changes warning |
|
||||
| Global beforeEach | NO | YES | Auth checks |
|
||||
| Route beforeEnter | NO | YES | Route-specific validation |
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **beforeRouteEnter runs before component creation** - No access to `this`
|
||||
2. **Use next(vm => ...) callback** - Only way to access component instance
|
||||
3. **Composition API has limitations** - Use onMounted or global guards instead
|
||||
4. **Consider route meta for pre-fetched data** - Clean separation of concerns
|
||||
5. **beforeRouteUpdate and beforeRouteLeave have component access** - They run when component exists
|
||||
|
||||
## Reference
|
||||
- [Vue Router In-Component Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards)
|
||||
- [Vue Router Navigation Resolution Flow](https://router.vuejs.org/guide/advanced/navigation-guards.html#the-full-navigation-resolution-flow)
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Async Navigation Guards Require Proper Promise Handling
|
||||
impact: MEDIUM
|
||||
impactDescription: Unawaited promises in guards cause navigation to complete before async checks finish, allowing unauthorized access or missing data
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, navigation-guards, async, promises]
|
||||
---
|
||||
|
||||
# Async Navigation Guards Require Proper Promise Handling
|
||||
|
||||
**Impact: MEDIUM** - Navigation guards that perform async operations (API calls, auth checks) must properly handle promises. If you don't await async operations or return the promise, navigation completes before your check finishes, potentially allowing unauthorized access or navigating with incomplete data.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Use async/await in navigation guards
|
||||
- [ ] Return the promise if not using async/await
|
||||
- [ ] Add loading states for long async operations
|
||||
- [ ] Implement timeouts for slow API calls
|
||||
- [ ] Handle errors to prevent navigation hanging
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// WRONG: Not awaiting - navigation proceeds immediately
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
checkAuth() // This returns a Promise but we're not waiting!
|
||||
// Navigation continues before checkAuth completes
|
||||
}
|
||||
})
|
||||
|
||||
// WRONG: Async function but forgot return
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
const isValid = await checkAuth()
|
||||
if (!isValid) {
|
||||
// This redirect might happen after navigation already completed!
|
||||
return '/login'
|
||||
}
|
||||
}
|
||||
// Missing return - implicitly returns undefined, allowing navigation
|
||||
})
|
||||
```
|
||||
|
||||
## Solution: Proper Async/Await Pattern
|
||||
|
||||
```javascript
|
||||
// CORRECT: Async function with proper returns
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
try {
|
||||
const isAuthenticated = await checkAuth()
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error)
|
||||
return { name: 'Error', params: { message: 'Authentication failed' } }
|
||||
}
|
||||
}
|
||||
// Explicitly return nothing to proceed
|
||||
return true
|
||||
})
|
||||
```
|
||||
|
||||
## Solution: Promise-Based Pattern (Alternative)
|
||||
|
||||
```javascript
|
||||
// CORRECT: Return promise explicitly
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
return checkAuth()
|
||||
.then(isAuthenticated => {
|
||||
if (!isAuthenticated) {
|
||||
return { name: 'Login' }
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Auth check failed:', error)
|
||||
return { name: 'Error' }
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Loading State During Async Guards
|
||||
|
||||
```javascript
|
||||
// app/composables/useNavigationLoading.js
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isNavigating = ref(false)
|
||||
|
||||
export function useNavigationLoading() {
|
||||
return { isNavigating }
|
||||
}
|
||||
|
||||
export function setupNavigationLoading(router) {
|
||||
router.beforeEach(() => {
|
||||
isNavigating.value = true
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
isNavigating.value = false
|
||||
})
|
||||
|
||||
router.onError(() => {
|
||||
isNavigating.value = false
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- App.vue -->
|
||||
<script setup>
|
||||
import { useNavigationLoading } from '@/composables/useNavigationLoading'
|
||||
|
||||
const { isNavigating } = useNavigationLoading()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoadingBar v-if="isNavigating" />
|
||||
<router-view />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Timeout Pattern for Slow APIs
|
||||
|
||||
```javascript
|
||||
// CORRECT: Add timeout to prevent indefinite waiting
|
||||
function withTimeout(promise, ms = 5000) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Request timeout')), ms)
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
try {
|
||||
const isValid = await withTimeout(checkAuth(), 5000)
|
||||
if (!isValid) {
|
||||
return '/login'
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message === 'Request timeout') {
|
||||
// Let user through but show warning
|
||||
console.warn('Auth check timed out')
|
||||
} else {
|
||||
return '/login'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Multiple Async Checks
|
||||
|
||||
```javascript
|
||||
// CORRECT: Run independent checks in parallel
|
||||
router.beforeEach(async (to, from) => {
|
||||
if (to.meta.requiresAuth && to.meta.requiresSubscription) {
|
||||
try {
|
||||
const [isAuthenticated, hasSubscription] = await Promise.all([
|
||||
checkAuth(),
|
||||
checkSubscription()
|
||||
])
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return '/login'
|
||||
}
|
||||
|
||||
if (!hasSubscription) {
|
||||
return '/subscribe'
|
||||
}
|
||||
} catch (error) {
|
||||
return '/error'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
```javascript
|
||||
router.beforeEach(async (to, from) => {
|
||||
try {
|
||||
// Your async logic here
|
||||
await performChecks(to)
|
||||
} catch (error) {
|
||||
// Always handle errors to prevent navigation from hanging
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
return '/login'
|
||||
}
|
||||
|
||||
if (error.response?.status === 403) {
|
||||
return '/forbidden'
|
||||
}
|
||||
|
||||
if (error.code === 'NETWORK_ERROR') {
|
||||
// Offline - maybe allow navigation but show warning
|
||||
return true
|
||||
}
|
||||
|
||||
// Unknown error - redirect to error page
|
||||
console.error('Navigation guard error:', error)
|
||||
return { name: 'Error', state: { error: error.message } }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Always await async operations** - Otherwise navigation proceeds immediately
|
||||
2. **Return values matter** - Return route to redirect, false to cancel, true/undefined to proceed
|
||||
3. **Handle all error cases** - Uncaught errors can hang navigation
|
||||
4. **Add timeouts** - Slow APIs shouldn't block navigation indefinitely
|
||||
5. **Show loading state** - Users need feedback during async checks
|
||||
6. **Parallelize independent checks** - Use Promise.all for better performance
|
||||
|
||||
## Reference
|
||||
- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
|
||||
- [Vue Router Navigation Failures](https://router.vuejs.org/guide/advanced/navigation-failures.html)
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
title: Navigation Guard Infinite Redirect Loops
|
||||
impact: HIGH
|
||||
impactDescription: Misconfigured navigation guards can trap users in infinite redirect loops, crashing the browser or making the app unusable
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, navigation-guards, redirect, debugging]
|
||||
---
|
||||
|
||||
# Navigation Guard Infinite Redirect Loops
|
||||
|
||||
**Impact: HIGH** - A common mistake in navigation guards is creating conditions that cause infinite redirects. Vue Router will detect this and show a warning, but in production, it can crash the browser or create a broken user experience.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Always check if already on target route before redirecting
|
||||
- [ ] Test guard logic with all possible navigation scenarios
|
||||
- [ ] Add route meta to control which routes need protection
|
||||
- [ ] Use Vue Router devtools to debug redirect chains
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// WRONG: Infinite loop - always redirects to login, even when on login!
|
||||
router.beforeEach((to, from) => {
|
||||
if (!isAuthenticated()) {
|
||||
return '/login' // Redirects to /login, which triggers guard again...
|
||||
}
|
||||
})
|
||||
|
||||
// WRONG: Circular redirect between two routes
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path === '/dashboard' && !hasProfile()) {
|
||||
return '/profile'
|
||||
}
|
||||
if (to.path === '/profile' && !isVerified()) {
|
||||
return '/dashboard' // Back to dashboard, which goes to profile...
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Error you'll see:**
|
||||
```
|
||||
[Vue Router warn]: Detected an infinite redirection in a navigation guard when going from "/" to "/login". Aborting to avoid a Stack Overflow.
|
||||
```
|
||||
|
||||
## Solution 1: Exclude Target Route
|
||||
|
||||
```javascript
|
||||
// CORRECT: Don't redirect if already going to login
|
||||
router.beforeEach((to, from) => {
|
||||
if (!isAuthenticated() && to.path !== '/login') {
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
||||
// CORRECT: Use route name for cleaner check
|
||||
router.beforeEach((to, from) => {
|
||||
const publicPages = ['Login', 'Register', 'ForgotPassword']
|
||||
|
||||
if (!isAuthenticated() && !publicPages.includes(to.name)) {
|
||||
return { name: 'Login' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Solution 2: Use Route Meta Fields
|
||||
|
||||
```javascript
|
||||
// router.js
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login,
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: Dashboard,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/public',
|
||||
name: 'PublicPage',
|
||||
component: PublicPage,
|
||||
meta: { requiresAuth: false }
|
||||
}
|
||||
]
|
||||
|
||||
// Guard checks meta field
|
||||
router.beforeEach((to, from) => {
|
||||
// Only redirect if route requires auth
|
||||
if (to.meta.requiresAuth && !isAuthenticated()) {
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Solution 3: Handle Redirect Chains Carefully
|
||||
|
||||
```javascript
|
||||
// CORRECT: Break potential circular redirects
|
||||
router.beforeEach((to, from) => {
|
||||
// Prevent redirect loops by tracking redirect depth
|
||||
const redirectCount = to.query._redirectCount || 0
|
||||
|
||||
if (redirectCount > 3) {
|
||||
console.error('Too many redirects, stopping at:', to.path)
|
||||
return '/error' // Escape hatch
|
||||
}
|
||||
|
||||
if (needsRedirect(to)) {
|
||||
return {
|
||||
path: getRedirectTarget(to),
|
||||
query: { ...to.query, _redirectCount: redirectCount + 1 }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Solution 4: Centralized Redirect Logic
|
||||
|
||||
```javascript
|
||||
// guards/auth.js
|
||||
export function createAuthGuard(router) {
|
||||
const publicRoutes = new Set(['Login', 'Register', 'ForgotPassword', 'ResetPassword'])
|
||||
const guestOnlyRoutes = new Set(['Login', 'Register'])
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
const isPublic = publicRoutes.has(to.name)
|
||||
const isGuestOnly = guestOnlyRoutes.has(to.name)
|
||||
const isLoggedIn = isAuthenticated()
|
||||
|
||||
// Not logged in, trying to access protected route
|
||||
if (!isLoggedIn && !isPublic) {
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
// Logged in, trying to access guest-only route (like login page)
|
||||
if (isLoggedIn && isGuestOnly) {
|
||||
return { name: 'Dashboard' }
|
||||
}
|
||||
|
||||
// All other cases: proceed
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging Redirect Loops
|
||||
|
||||
```javascript
|
||||
// Add logging to understand the redirect chain
|
||||
router.beforeEach((to, from) => {
|
||||
console.log(`Navigation: ${from.path} -> ${to.path}`)
|
||||
console.log('Auth state:', isAuthenticated())
|
||||
console.log('Route meta:', to.meta)
|
||||
|
||||
// Your guard logic here
|
||||
})
|
||||
|
||||
// Or use afterEach for confirmed navigations
|
||||
router.afterEach((to, from) => {
|
||||
console.log(`Navigated: ${from.path} -> ${to.path}`)
|
||||
})
|
||||
```
|
||||
|
||||
## Common Redirect Loop Patterns
|
||||
|
||||
| Pattern | Problem | Fix |
|
||||
|---------|---------|-----|
|
||||
| Auth check without exclusion | Login redirects to login | Exclude `/login` from check |
|
||||
| Role-based with circular deps | Admin -> User -> Admin | Use single source of truth for role requirements |
|
||||
| Onboarding flow | Step 1 -> Step 2 -> Step 1 | Track completion state properly |
|
||||
| Redirect query handling | Reading redirect creates new redirect | Process redirect only once |
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Always exclude the target route** - Never redirect to a route that would trigger the same redirect
|
||||
2. **Use route meta fields** - Cleaner than path string comparisons
|
||||
3. **Test edge cases** - Direct URL access, refresh, back button
|
||||
4. **Add logging during development** - Helps trace redirect chains
|
||||
5. **Have an escape hatch** - Error page or max redirect count
|
||||
|
||||
## Reference
|
||||
- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
|
||||
- [Vue Router Route Meta Fields](https://router.vuejs.org/guide/advanced/meta.html)
|
||||
@@ -0,0 +1,150 @@
|
||||
---
|
||||
title: Vue Router Navigation Guard next() Function Deprecated
|
||||
impact: HIGH
|
||||
impactDescription: Using the deprecated next() function incorrectly causes navigation to hang, infinite loops, or silent failures
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, navigation-guards, migration, async]
|
||||
---
|
||||
|
||||
# Vue Router Navigation Guard next() Function Deprecated
|
||||
|
||||
**Impact: HIGH** - The third `next()` argument in navigation guards is deprecated in Vue Router 4. While still supported for backward compatibility, using it incorrectly is one of the most common sources of bugs: calling it multiple times, forgetting to call it, or calling it conditionally without proper logic.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Refactor guards to use return-based syntax instead of next()
|
||||
- [ ] Remove all next() calls from navigation guards
|
||||
- [ ] Use async/await pattern for asynchronous checks
|
||||
- [ ] Return false to cancel, return route to redirect, return nothing to proceed
|
||||
|
||||
## The Problem
|
||||
|
||||
```javascript
|
||||
// WRONG: Using deprecated next() function
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (!isAuthenticated) {
|
||||
next('/login') // Easy to forget this call
|
||||
}
|
||||
// BUG: next() not called when authenticated - navigation hangs!
|
||||
})
|
||||
|
||||
// WRONG: Multiple next() calls
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (!isAuthenticated) {
|
||||
next('/login')
|
||||
}
|
||||
next() // BUG: Called twice when not authenticated!
|
||||
})
|
||||
|
||||
// WRONG: next() in async code without proper handling
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const user = await fetchUser()
|
||||
if (!user) {
|
||||
next('/login')
|
||||
}
|
||||
next() // Still gets called even after redirect!
|
||||
})
|
||||
```
|
||||
|
||||
## Solution: Use Return-Based Guards
|
||||
|
||||
```javascript
|
||||
// CORRECT: Return-based syntax (modern Vue Router 4+)
|
||||
router.beforeEach((to, from) => {
|
||||
if (!isAuthenticated) {
|
||||
return '/login' // Redirect
|
||||
}
|
||||
// Return nothing (undefined) to proceed
|
||||
})
|
||||
|
||||
// CORRECT: Return false to cancel navigation
|
||||
router.beforeEach((to, from) => {
|
||||
if (hasUnsavedChanges) {
|
||||
return false // Cancel navigation
|
||||
}
|
||||
})
|
||||
|
||||
// CORRECT: Async with return-based syntax
|
||||
router.beforeEach(async (to, from) => {
|
||||
const user = await fetchUser()
|
||||
if (!user) {
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
// Proceed with navigation
|
||||
})
|
||||
```
|
||||
|
||||
## Return Values Explained
|
||||
|
||||
```javascript
|
||||
router.beforeEach((to, from) => {
|
||||
// Return nothing/undefined - allow navigation
|
||||
return
|
||||
|
||||
// Return false - cancel navigation, stay on current route
|
||||
return false
|
||||
|
||||
// Return string path - redirect to path
|
||||
return '/login'
|
||||
|
||||
// Return route object - redirect with full control
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
|
||||
// Return Error - cancel and trigger router.onError()
|
||||
return new Error('Navigation cancelled')
|
||||
})
|
||||
```
|
||||
|
||||
## If You Must Use next() (Legacy Code)
|
||||
|
||||
If maintaining legacy code that uses `next()`, follow these rules strictly:
|
||||
|
||||
```javascript
|
||||
// CORRECT: Exactly one next() call per code path
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (!isAuthenticated) {
|
||||
next('/login')
|
||||
return // CRITICAL: Exit after calling next()
|
||||
}
|
||||
|
||||
if (!hasPermission(to)) {
|
||||
next('/forbidden')
|
||||
return // CRITICAL: Exit after calling next()
|
||||
}
|
||||
|
||||
next() // Only reached if all checks pass
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling Pattern
|
||||
|
||||
```javascript
|
||||
router.beforeEach(async (to, from) => {
|
||||
try {
|
||||
await validateAccess(to)
|
||||
// Proceed
|
||||
} catch (error) {
|
||||
if (error.status === 401) {
|
||||
return '/login'
|
||||
}
|
||||
if (error.status === 403) {
|
||||
return '/forbidden'
|
||||
}
|
||||
// Log error and proceed anyway (or return false)
|
||||
console.error('Access validation failed:', error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Prefer return-based syntax** - Cleaner, less error-prone, modern standard
|
||||
2. **next() must be called exactly once** - If using legacy syntax, ensure single call per path
|
||||
3. **Always return/exit after redirect** - Prevent multiple navigation actions
|
||||
4. **Async guards work naturally** - Just return the redirect route or nothing
|
||||
5. **Test all code paths** - Each branch must result in either return or next()
|
||||
|
||||
## Reference
|
||||
- [Vue Router Navigation Guards](https://router.vuejs.org/guide/advanced/navigation-guards.html)
|
||||
- [RFC: Remove next() from Navigation Guards](https://github.com/vuejs/rfcs/discussions/302)
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
title: Route Param Changes Do Not Trigger Lifecycle Hooks
|
||||
impact: HIGH
|
||||
impactDescription: Navigating between routes with different params reuses the component instance, skipping created/mounted hooks and leaving stale data
|
||||
type: gotcha
|
||||
tags: [vue3, vue-router, lifecycle, params, reactivity]
|
||||
---
|
||||
|
||||
# Route Param Changes Do Not Trigger Lifecycle Hooks
|
||||
|
||||
**Impact: HIGH** - When navigating between routes that use the same component (e.g., `/users/1` to `/users/2`), Vue Router reuses the existing component instance for performance. This means `onMounted`, `created`, and other lifecycle hooks do NOT fire, leaving you with stale data from the previous route.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Use `watch` on route params for data fetching
|
||||
- [ ] Or use `onBeforeRouteUpdate` in-component guard
|
||||
- [ ] Or use `:key="route.params.id"` to force re-creation (less efficient)
|
||||
- [ ] Never rely solely on `onMounted` for route-param-dependent data
|
||||
|
||||
## The Problem
|
||||
|
||||
```vue
|
||||
<!-- UserProfile.vue - Used for /users/:id -->
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const user = ref(null)
|
||||
|
||||
// BUG: Only runs once when component first mounts!
|
||||
// Navigating from /users/1 to /users/2 does NOT trigger this
|
||||
onMounted(async () => {
|
||||
user.value = await fetchUser(route.params.id)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Still shows User 1 data when navigating to /users/2! -->
|
||||
<h1>{{ user?.name }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Scenario:**
|
||||
1. Visit `/users/1` - Component mounts, fetches User 1 data
|
||||
2. Navigate to `/users/2` - Component is REUSED, onMounted doesn't run
|
||||
3. UI still shows User 1's data!
|
||||
|
||||
## Solution 1: Watch Route Params (Recommended)
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const user = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// Watch for param changes - handles both initial load and navigation
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (newId) => {
|
||||
loading.value = true
|
||||
user.value = await fetchUser(newId)
|
||||
loading.value = false
|
||||
},
|
||||
{ immediate: true } // Run immediately for initial load
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
## Solution 2: Use onBeforeRouteUpdate Guard
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const user = ref(null)
|
||||
|
||||
async function loadUser(id) {
|
||||
user.value = await fetchUser(id)
|
||||
}
|
||||
|
||||
// Initial load
|
||||
onMounted(() => loadUser(route.params.id))
|
||||
|
||||
// Handle param changes within same route
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
if (to.params.id !== from.params.id) {
|
||||
await loadUser(to.params.id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Solution 3: Force Component Re-creation with Key
|
||||
|
||||
```vue
|
||||
<!-- App.vue or parent component -->
|
||||
<template>
|
||||
<router-view :key="$route.fullPath" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**Tradeoffs:**
|
||||
- Simple but less performant
|
||||
- Destroys and recreates component on every param change
|
||||
- Loses component state
|
||||
- Use only when component state should reset completely
|
||||
|
||||
## Solution 4: Composable for Route-Reactive Data
|
||||
|
||||
```javascript
|
||||
// composables/useRouteData.js
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export function useRouteData(paramName, fetcher) {
|
||||
const route = useRoute()
|
||||
const data = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
|
||||
watch(
|
||||
() => route.params[paramName],
|
||||
async (id) => {
|
||||
if (!id) return
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
data.value = await fetcher(id)
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return { data, loading, error }
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Usage in component -->
|
||||
<script setup>
|
||||
import { useRouteData } from '@/composables/useRouteData'
|
||||
import { fetchUser } from '@/api/users'
|
||||
|
||||
const { data: user, loading, error } = useRouteData('id', fetchUser)
|
||||
</script>
|
||||
```
|
||||
|
||||
## What Triggers vs. What Doesn't
|
||||
|
||||
| Navigation Type | Lifecycle Hooks | beforeRouteUpdate | Watch on params |
|
||||
|----------------|-----------------|-------------------|-----------------|
|
||||
| `/users/1` to `/posts/1` | YES | NO | YES |
|
||||
| `/users/1` to `/users/2` | NO | YES | YES |
|
||||
| `/users/1?tab=a` to `/users/1?tab=b` | NO | YES | NO (different watch) |
|
||||
| `/users/1` to `/users/1` (same) | NO | NO | NO |
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Same route, different params = same component instance** - This is a performance optimization
|
||||
2. **Lifecycle hooks only fire once** - When component first mounts
|
||||
3. **Use `watch` with `immediate: true`** - Covers both initial load and updates
|
||||
4. **`onBeforeRouteUpdate` is navigation-aware** - Good for data that must load before view updates
|
||||
5. **`:key="route.fullPath"` is a sledgehammer** - Use only when necessary
|
||||
|
||||
## Reference
|
||||
- [Vue Router Dynamic Route Matching](https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes)
|
||||
- [Vue School: Reacting to Param Changes](https://vueschool.io/lessons/reacting-to-param-changes)
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
title: Simple Hash Routing Requires Event Listener Cleanup
|
||||
impact: MEDIUM
|
||||
impactDescription: When implementing basic routing without Vue Router, forgetting to remove hashchange listeners causes memory leaks and multiple handler execution
|
||||
type: gotcha
|
||||
tags: [vue3, routing, events, memory-leak, cleanup]
|
||||
---
|
||||
|
||||
# Simple Hash Routing Requires Event Listener Cleanup
|
||||
|
||||
**Impact: MEDIUM** - When implementing basic client-side routing without Vue Router (using hash-based routing with `hashchange` events), you must clean up event listeners when the component unmounts. Failure to do so causes memory leaks and can result in multiple handlers firing after the component is recreated.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Store event listener reference for cleanup
|
||||
- [ ] Use onUnmounted to remove event listener
|
||||
- [ ] Consider using Vue Router instead for production apps
|
||||
- [ ] Test component mount/unmount cycles
|
||||
|
||||
## The Problem
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Home from './Home.vue'
|
||||
import About from './About.vue'
|
||||
|
||||
const routes = {
|
||||
'/': Home,
|
||||
'/about': About
|
||||
}
|
||||
|
||||
const currentPath = ref(window.location.hash)
|
||||
|
||||
// BUG: Event listener is never removed!
|
||||
// Each time this component mounts, a NEW listener is added
|
||||
// After mounting 5 times, you have 5 listeners running
|
||||
window.addEventListener('hashchange', () => {
|
||||
currentPath.value = window.location.hash
|
||||
})
|
||||
|
||||
const currentView = computed(() => {
|
||||
return routes[currentPath.value.slice(1) || '/']
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Component mounts, adds listener
|
||||
2. Component unmounts (e.g., route change, v-if toggle)
|
||||
3. Component mounts again, adds ANOTHER listener
|
||||
4. Now TWO listeners respond to each hash change
|
||||
5. Eventually causes performance issues and memory leaks
|
||||
|
||||
## Solution: Proper Cleanup with onUnmounted
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import Home from './Home.vue'
|
||||
import About from './About.vue'
|
||||
import NotFound from './NotFound.vue'
|
||||
|
||||
const routes = {
|
||||
'/': Home,
|
||||
'/about': About
|
||||
}
|
||||
|
||||
const currentPath = ref(window.location.hash)
|
||||
|
||||
// Store handler reference for cleanup
|
||||
function handleHashChange() {
|
||||
currentPath.value = window.location.hash
|
||||
}
|
||||
|
||||
// Add listener
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
|
||||
// CRITICAL: Remove listener on unmount
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('hashchange', handleHashChange)
|
||||
})
|
||||
|
||||
const currentView = computed(() => {
|
||||
return routes[currentPath.value.slice(1) || '/'] || NotFound
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Solution: Using Options API
|
||||
|
||||
```vue
|
||||
<script>
|
||||
import Home from './Home.vue'
|
||||
import About from './About.vue'
|
||||
import NotFound from './NotFound.vue'
|
||||
|
||||
const routes = {
|
||||
'/': Home,
|
||||
'/about': About
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentPath: window.location.hash
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentView() {
|
||||
return routes[this.currentPath.slice(1) || '/'] || NotFound
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Store bound handler for cleanup
|
||||
this.hashHandler = () => {
|
||||
this.currentPath = window.location.hash
|
||||
}
|
||||
window.addEventListener('hashchange', this.hashHandler)
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
// Clean up
|
||||
window.removeEventListener('hashchange', this.hashHandler)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Solution: Composable for Reusable Hash Routing
|
||||
|
||||
```javascript
|
||||
// composables/useHashRouter.js
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
|
||||
export function useHashRouter(routes, notFoundComponent = null) {
|
||||
const currentPath = ref(window.location.hash)
|
||||
|
||||
function handleHashChange() {
|
||||
currentPath.value = window.location.hash
|
||||
}
|
||||
|
||||
// Setup
|
||||
window.addEventListener('hashchange', handleHashChange)
|
||||
|
||||
// Cleanup - handled automatically when component unmounts
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('hashchange', handleHashChange)
|
||||
})
|
||||
|
||||
const currentView = computed(() => {
|
||||
const path = currentPath.value.slice(1) || '/'
|
||||
return routes[path] || notFoundComponent
|
||||
})
|
||||
|
||||
function navigate(path) {
|
||||
window.location.hash = path
|
||||
}
|
||||
|
||||
return {
|
||||
currentPath,
|
||||
currentView,
|
||||
navigate
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- Usage -->
|
||||
<script setup>
|
||||
import { useHashRouter } from '@/composables/useHashRouter'
|
||||
import Home from './Home.vue'
|
||||
import About from './About.vue'
|
||||
import NotFound from './NotFound.vue'
|
||||
|
||||
const { currentView } = useHashRouter({
|
||||
'/': Home,
|
||||
'/about': About
|
||||
}, NotFound)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="currentView" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## When to Use Simple Routing vs Vue Router
|
||||
|
||||
| Use Simple Hash Routing | Use Vue Router |
|
||||
|------------------------|----------------|
|
||||
| Learning/prototyping | Production apps |
|
||||
| Very simple apps (2-3 pages) | Nested routes needed |
|
||||
| No build step available | Navigation guards needed |
|
||||
| Bundle size critical | Lazy loading needed |
|
||||
| Static hosting only | History mode (clean URLs) |
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Always clean up event listeners** - Use onUnmounted or beforeUnmount
|
||||
2. **Store handler reference** - Anonymous functions can't be removed
|
||||
3. **Consider Vue Router for real apps** - It handles cleanup automatically
|
||||
4. **Test unmount scenarios** - v-if toggling, hot module replacement
|
||||
5. **Composables help encapsulate cleanup logic** - Reusable and automatic
|
||||
|
||||
## Reference
|
||||
- [Vue.js Routing Documentation](https://vuejs.org/guide/scaling-up/routing.html)
|
||||
- [Vue Router Official Library](https://router.vuejs.org/)
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: Use Vue Router Library for Production Applications
|
||||
impact: LOW
|
||||
impactDescription: Simple hash routing lacks essential features for production SPAs; Vue Router provides navigation guards, lazy loading, and proper history management
|
||||
type: best-practice
|
||||
tags: [vue3, vue-router, spa, production, architecture]
|
||||
---
|
||||
|
||||
# Use Vue Router Library for Production Applications
|
||||
|
||||
**Impact: LOW** - While you can implement basic routing with hash changes and dynamic components, the official Vue Router library should be used for any production single-page application. It provides essential features like navigation guards, nested routes, lazy loading, and proper browser history integration that are tedious and error-prone to implement manually.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Install Vue Router for production SPAs
|
||||
- [ ] Use simple routing only for learning or tiny prototypes
|
||||
- [ ] Leverage built-in features: guards, lazy loading, meta fields
|
||||
- [ ] Consider router-based state and data loading patterns
|
||||
|
||||
## When Simple Routing is Acceptable
|
||||
|
||||
```vue
|
||||
<!-- Only for: learning, prototypes, or micro-apps with 2-3 pages -->
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import Home from './Home.vue'
|
||||
import About from './About.vue'
|
||||
|
||||
const routes = { '/': Home, '/about': About }
|
||||
const currentPath = ref(window.location.hash.slice(1) || '/')
|
||||
|
||||
window.addEventListener('hashchange', () => {
|
||||
currentPath.value = window.location.hash.slice(1) || '/'
|
||||
})
|
||||
|
||||
const currentView = computed(() => routes[currentPath.value])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav>
|
||||
<a href="#/">Home</a>
|
||||
<a href="#/about">About</a>
|
||||
</nav>
|
||||
<component :is="currentView" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Why Vue Router for Production
|
||||
|
||||
### Features You'd Have to Implement Manually
|
||||
|
||||
| Feature | Simple Routing | Vue Router |
|
||||
|---------|---------------|------------|
|
||||
| Navigation guards | Manual, error-prone | Built-in, composable |
|
||||
| Nested routes | Complex to implement | Native support |
|
||||
| Route params | Parse manually | Automatic extraction |
|
||||
| Lazy loading | DIY with dynamic imports | Built-in with code splitting |
|
||||
| History mode (clean URLs) | Requires server config + manual | Built-in |
|
||||
| Scroll behavior | Manual | Configurable |
|
||||
| Route transitions | DIY | Integrated with Transition |
|
||||
| Active link styling | Manual class toggling | `router-link-active` class |
|
||||
| Programmatic navigation | `location.hash = ...` | `router.push()`, `router.replace()` |
|
||||
| Route meta fields | N/A | Built-in |
|
||||
|
||||
## Production Setup with Vue Router
|
||||
|
||||
```javascript
|
||||
// router/index.js
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/Home.vue'), // Lazy loaded
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/Dashboard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/Settings.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/users/:id',
|
||||
name: 'UserProfile',
|
||||
component: () => import('@/views/UserProfile.vue'),
|
||||
props: true // Pass params as props
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
return savedPosition || { top: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
// Global navigation guard
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.meta.requiresAuth && !isAuthenticated()) {
|
||||
return { name: 'Login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
```javascript
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- App.vue -->
|
||||
<template>
|
||||
<nav>
|
||||
<router-link to="/">Home</router-link>
|
||||
<router-link to="/dashboard">Dashboard</router-link>
|
||||
</nav>
|
||||
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Modern Vue Router Features (2025+)
|
||||
|
||||
```javascript
|
||||
// Data Loading API (Vue Router 4.2+)
|
||||
const routes = [
|
||||
{
|
||||
path: '/users/:id',
|
||||
component: UserProfile,
|
||||
// Load data at route level
|
||||
loader: async (route) => {
|
||||
return { user: await fetchUser(route.params.id) }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// View Transitions API integration
|
||||
const router = createRouter({
|
||||
// Enable native browser view transitions
|
||||
// Requires browser support (Chrome 111+)
|
||||
})
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
1. **Use Vue Router for any app beyond a prototype** - The features are essential
|
||||
2. **Simple routing is for learning** - Understand the concepts, then use the library
|
||||
3. **Lazy loading is critical for bundle size** - Vue Router makes it trivial
|
||||
4. **Navigation guards prevent security issues** - Hard to get right manually
|
||||
5. **History mode requires Vue Router** - Clean URLs need proper handling
|
||||
6. **New features keep coming** - Data Loading API, View Transitions
|
||||
|
||||
## Reference
|
||||
- [Vue.js Routing Guide](https://vuejs.org/guide/scaling-up/routing.html)
|
||||
- [Vue Router Documentation](https://router.vuejs.org/)
|
||||
- [Vue Router Getting Started](https://router.vuejs.org/guide/)
|
||||
Reference in New Issue
Block a user