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,233 @@
|
||||
---
|
||||
title: Use KeepAlive to Preserve Dynamic Component State
|
||||
impact: MEDIUM
|
||||
impactDescription: Dynamic component switching destroys and recreates components, losing all internal state unless wrapped in KeepAlive
|
||||
type: best-practice
|
||||
tags: [vue3, dynamic-components, keepalive, component-is, state-preservation, performance]
|
||||
---
|
||||
|
||||
# Use KeepAlive to Preserve Dynamic Component State
|
||||
|
||||
**Impact: MEDIUM** - When switching between components using `<component :is="...">`, Vue destroys the old component and creates a new one. All internal state (form inputs, scroll position, fetched data) is lost. Wrapping dynamic components in `<KeepAlive>` caches them and preserves their state.
|
||||
|
||||
## Task Checklist
|
||||
|
||||
- [ ] Wrap `<component :is>` with `<KeepAlive>` when state preservation is needed
|
||||
- [ ] Use `include` and `exclude` to control which components are cached
|
||||
- [ ] Use `max` to limit cache size and prevent memory issues
|
||||
- [ ] Implement `onActivated`/`onDeactivated` hooks for cache-aware logic
|
||||
- [ ] Consider NOT using KeepAlive when fresh state is desired
|
||||
|
||||
## The Problem: State Loss
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import TabA from './TabA.vue'
|
||||
import TabB from './TabB.vue'
|
||||
|
||||
const currentTab = shallowRef(TabA)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="currentTab = TabA">Tab A</button>
|
||||
<button @click="currentTab = TabB">Tab B</button>
|
||||
|
||||
<!-- State is lost when switching tabs! -->
|
||||
<component :is="currentTab" />
|
||||
</template>
|
||||
```
|
||||
|
||||
If TabA has a form with user input, switching to TabB and back resets all input.
|
||||
|
||||
## Solution: KeepAlive
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<button @click="currentTab = TabA">Tab A</button>
|
||||
<button @click="currentTab = TabB">Tab B</button>
|
||||
|
||||
<!-- State is preserved when switching -->
|
||||
<KeepAlive>
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
```
|
||||
|
||||
Now TabA's state persists even when TabB is displayed.
|
||||
|
||||
## Controlling What Gets Cached
|
||||
|
||||
### Include/Exclude by Name
|
||||
|
||||
Only cache specific components:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Only cache components named 'TabA' or 'TabB' -->
|
||||
<KeepAlive include="TabA,TabB">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- Cache all except 'HeavyComponent' -->
|
||||
<KeepAlive exclude="HeavyComponent">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- Using regex -->
|
||||
<KeepAlive :include="/^Tab/">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
|
||||
<!-- Using array -->
|
||||
<KeepAlive :include="['TabA', 'TabB', 'Settings']">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Important:** Components must have a `name` option to be matched:
|
||||
|
||||
```vue
|
||||
<!-- TabA.vue -->
|
||||
<script>
|
||||
export default {
|
||||
name: 'TabA' // Required for include/exclude matching
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
// ...composition API code
|
||||
</script>
|
||||
```
|
||||
|
||||
Or in Vue 3.3+ with `<script setup>`:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'TabA'
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### Limit Cache Size
|
||||
|
||||
Prevent memory issues with many cached components:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- Only keep last 5 components in cache -->
|
||||
<KeepAlive :max="5">
|
||||
<component :is="currentTab" />
|
||||
</KeepAlive>
|
||||
</template>
|
||||
```
|
||||
|
||||
When cache exceeds `max`, the least recently accessed component is destroyed.
|
||||
|
||||
## Lifecycle Hooks: onActivated and onDeactivated
|
||||
|
||||
Cached components need special lifecycle hooks:
|
||||
|
||||
```vue
|
||||
<!-- TabA.vue -->
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
|
||||
|
||||
// Only called on first mount, NOT when switching back
|
||||
onMounted(() => {
|
||||
console.log('TabA mounted (once)')
|
||||
})
|
||||
|
||||
// Only called when truly destroyed, NOT when switching away
|
||||
onUnmounted(() => {
|
||||
console.log('TabA unmounted')
|
||||
})
|
||||
|
||||
// Called EVERY time component becomes visible
|
||||
onActivated(() => {
|
||||
console.log('TabA activated')
|
||||
// Refresh data, resume timers, etc.
|
||||
fetchLatestData()
|
||||
})
|
||||
|
||||
// Called EVERY time component is hidden (but kept in cache)
|
||||
onDeactivated(() => {
|
||||
console.log('TabA deactivated')
|
||||
// Pause timers, save draft, etc.
|
||||
pauseAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**Common use cases for activation hooks:**
|
||||
- Refresh stale data when returning to a tab
|
||||
- Resume/pause video or audio playback
|
||||
- Reconnect/disconnect WebSocket connections
|
||||
- Save/restore scroll position
|
||||
- Track analytics for tab views
|
||||
|
||||
## KeepAlive with Vue Router
|
||||
|
||||
For route-based caching:
|
||||
|
||||
```vue
|
||||
<!-- App.vue -->
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive include="Dashboard,Settings">
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</template>
|
||||
```
|
||||
|
||||
**With transition:**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="fade" mode="out-in">
|
||||
<KeepAlive>
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
</router-view>
|
||||
</template>
|
||||
```
|
||||
|
||||
## When NOT to Use KeepAlive
|
||||
|
||||
Don't cache when:
|
||||
|
||||
```vue
|
||||
<!-- Fresh data needed each time -->
|
||||
<template>
|
||||
<!-- NO KeepAlive - want fresh search results each visit -->
|
||||
<component :is="currentView" />
|
||||
</template>
|
||||
```
|
||||
|
||||
- Form should reset between visits
|
||||
- Data must be fresh (real-time dashboards)
|
||||
- Component has significant memory footprint
|
||||
- Security-sensitive data should be cleared
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { shallowRef } from 'vue'
|
||||
import TabA from './TabA.vue'
|
||||
import TabB from './TabB.vue'
|
||||
|
||||
// Use shallowRef for component references
|
||||
// Regular ref would deeply track the component object unnecessarily
|
||||
const currentTab = shallowRef(TabA)
|
||||
</script>
|
||||
```
|
||||
|
||||
## Reference
|
||||
- [Vue.js KeepAlive](https://vuejs.org/guide/built-ins/keep-alive.html)
|
||||
- [Vue.js Dynamic Components](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components)
|
||||
Reference in New Issue
Block a user