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>
250 lines
5.7 KiB
Markdown
250 lines
5.7 KiB
Markdown
---
|
|
title: Avoid Prop Drilling - Use Provide/Inject for Deep Component Trees
|
|
impact: MEDIUM
|
|
impactDescription: Passing props through many layers creates maintenance burden and tight coupling between intermediate components
|
|
type: best-practice
|
|
tags: [vue3, props, provide-inject, component-design, state-management, architecture]
|
|
---
|
|
|
|
# Avoid Prop Drilling - Use Provide/Inject for Deep Component Trees
|
|
|
|
**Impact: MEDIUM** - Prop drilling occurs when you pass props through multiple component layers just to reach a deeply nested child. This creates tight coupling, makes refactoring difficult, and clutters intermediate components with props they don't use.
|
|
|
|
Vue's provide/inject API allows ancestor components to share data with any descendant, regardless of nesting depth.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Identify when props pass through 2+ intermediate components unchanged
|
|
- [ ] Use provide/inject for data needed by deeply nested descendants
|
|
- [ ] Use Pinia for global state shared across unrelated component trees
|
|
- [ ] Keep props for direct parent-child relationships
|
|
- [ ] Document provided values at the provider level
|
|
|
|
## The Problem: Prop Drilling
|
|
|
|
```vue
|
|
<!-- App.vue -->
|
|
<template>
|
|
<MainLayout :user="user" :theme="theme" :locale="locale" />
|
|
</template>
|
|
```
|
|
|
|
```vue
|
|
<!-- MainLayout.vue - Doesn't use these props, just passes them -->
|
|
<template>
|
|
<Sidebar :user="user" :theme="theme" />
|
|
<Content :user="user" :locale="locale" />
|
|
</template>
|
|
```
|
|
|
|
```vue
|
|
<!-- Sidebar.vue - Still drilling... -->
|
|
<template>
|
|
<UserMenu :user="user" />
|
|
<ThemeToggle :theme="theme" />
|
|
</template>
|
|
```
|
|
|
|
```vue
|
|
<!-- UserMenu.vue - Finally uses user prop -->
|
|
<template>
|
|
<div>{{ user.name }}</div>
|
|
</template>
|
|
```
|
|
|
|
**Problems:**
|
|
1. `MainLayout` and `Sidebar` are cluttered with props they don't use
|
|
2. Adding a new shared value requires updating every component in the chain
|
|
3. Removing a deeply nested component requires updating all ancestors
|
|
4. Difficult to trace where data originates
|
|
|
|
## Solution: Provide/Inject
|
|
|
|
**Correct - Provider (ancestor):**
|
|
```vue
|
|
<!-- App.vue -->
|
|
<script setup>
|
|
import { provide, ref, readonly } from 'vue'
|
|
|
|
const user = ref({ name: 'John', role: 'admin' })
|
|
const theme = ref('dark')
|
|
const locale = ref('en')
|
|
|
|
// Provide to all descendants
|
|
provide('user', readonly(user)) // readonly prevents mutations
|
|
provide('theme', theme)
|
|
provide('locale', locale)
|
|
|
|
// Provide update functions if needed
|
|
provide('updateTheme', (newTheme) => {
|
|
theme.value = newTheme
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<MainLayout />
|
|
</template>
|
|
```
|
|
|
|
**Correct - Intermediate components are now clean:**
|
|
```vue
|
|
<!-- MainLayout.vue - No props needed -->
|
|
<template>
|
|
<Sidebar />
|
|
<Content />
|
|
</template>
|
|
```
|
|
|
|
```vue
|
|
<!-- Sidebar.vue - No props needed -->
|
|
<template>
|
|
<UserMenu />
|
|
<ThemeToggle />
|
|
</template>
|
|
```
|
|
|
|
**Correct - Consumer (descendant):**
|
|
```vue
|
|
<!-- UserMenu.vue -->
|
|
<script setup>
|
|
import { inject } from 'vue'
|
|
|
|
// Inject from any ancestor
|
|
const user = inject('user')
|
|
</script>
|
|
|
|
<template>
|
|
<div>{{ user.name }}</div>
|
|
</template>
|
|
```
|
|
|
|
```vue
|
|
<!-- ThemeToggle.vue -->
|
|
<script setup>
|
|
import { inject } from 'vue'
|
|
|
|
const theme = inject('theme')
|
|
const updateTheme = inject('updateTheme')
|
|
|
|
function toggleTheme() {
|
|
updateTheme(theme.value === 'dark' ? 'light' : 'dark')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<button @click="toggleTheme">
|
|
Current: {{ theme }}
|
|
</button>
|
|
</template>
|
|
```
|
|
|
|
## Best Practices for Provide/Inject
|
|
|
|
### 1. Use Symbol Keys for Large Apps
|
|
|
|
Avoid string key collisions with symbols:
|
|
|
|
```js
|
|
// keys.js
|
|
export const UserKey = Symbol('user')
|
|
export const ThemeKey = Symbol('theme')
|
|
```
|
|
|
|
```vue
|
|
<script setup>
|
|
import { provide } from 'vue'
|
|
import { UserKey, ThemeKey } from './keys'
|
|
|
|
provide(UserKey, user)
|
|
provide(ThemeKey, theme)
|
|
</script>
|
|
```
|
|
|
|
```vue
|
|
<script setup>
|
|
import { inject } from 'vue'
|
|
import { UserKey } from './keys'
|
|
|
|
const user = inject(UserKey)
|
|
</script>
|
|
```
|
|
|
|
### 2. Provide Default Values
|
|
|
|
Handle cases where no ancestor provides the value:
|
|
|
|
```vue
|
|
<script setup>
|
|
import { inject } from 'vue'
|
|
|
|
// With default value
|
|
const theme = inject('theme', 'light')
|
|
|
|
// With factory function for objects (avoids shared reference)
|
|
const config = inject('config', () => ({ debug: false }), true)
|
|
</script>
|
|
```
|
|
|
|
### 3. Use Readonly for Data Safety
|
|
|
|
Prevent descendants from mutating provided data:
|
|
|
|
```vue
|
|
<script setup>
|
|
import { provide, ref, readonly } from 'vue'
|
|
|
|
const user = ref({ name: 'John' })
|
|
|
|
// Descendants can read but not mutate
|
|
provide('user', readonly(user))
|
|
|
|
// Provide separate method for updates
|
|
provide('updateUser', (updates) => {
|
|
Object.assign(user.value, updates)
|
|
})
|
|
</script>
|
|
```
|
|
|
|
### 4. Provide Computed Values for Reactivity
|
|
|
|
```vue
|
|
<script setup>
|
|
import { provide, computed } from 'vue'
|
|
|
|
const items = ref([1, 2, 3])
|
|
|
|
// Descendants will reactively update
|
|
provide('itemCount', computed(() => items.value.length))
|
|
</script>
|
|
```
|
|
|
|
## When to Use What
|
|
|
|
| Scenario | Solution |
|
|
|----------|----------|
|
|
| Direct parent-child | Props |
|
|
| 1-2 levels deep | Props (drilling is acceptable) |
|
|
| Deep nesting, same component tree | Provide/Inject |
|
|
| Unrelated component trees | Pinia (state management) |
|
|
| Cross-app global state | Pinia |
|
|
| Plugin configuration | Provide/Inject from plugin install |
|
|
|
|
## Provide/Inject vs Pinia
|
|
|
|
**Provide/Inject:**
|
|
- Scoped to component subtree
|
|
- Great for component library internals
|
|
- No DevTools support
|
|
- Ancestor-descendant relationships only
|
|
|
|
**Pinia:**
|
|
- Global, accessible anywhere
|
|
- Excellent DevTools integration
|
|
- Better for application state
|
|
- Works across unrelated components
|
|
|
|
## Reference
|
|
- [Vue.js Provide/Inject](https://vuejs.org/guide/components/provide-inject.html)
|
|
- [Vue.js - Prop Drilling](https://vuejs.org/guide/components/provide-inject.html#prop-drilling)
|
|
- [Pinia Documentation](https://pinia.vuejs.org/)
|