Files
agent-skills/skills/vue-best-practices/reference/avoid-prop-drilling-use-provide-inject.md
Jason Woltje f5792c40be 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>
2026-02-16 16:27:42 -06:00

5.7 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Avoid Prop Drilling - Use Provide/Inject for Deep Component Trees MEDIUM Passing props through many layers creates maintenance burden and tight coupling between intermediate components best-practice
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

<!-- App.vue -->
<template>
  <MainLayout :user="user" :theme="theme" :locale="locale" />
</template>
<!-- MainLayout.vue - Doesn't use these props, just passes them -->
<template>
  <Sidebar :user="user" :theme="theme" />
  <Content :user="user" :locale="locale" />
</template>
<!-- Sidebar.vue - Still drilling... -->
<template>
  <UserMenu :user="user" />
  <ThemeToggle :theme="theme" />
</template>
<!-- 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):

<!-- 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:

<!-- MainLayout.vue - No props needed -->
<template>
  <Sidebar />
  <Content />
</template>
<!-- Sidebar.vue - No props needed -->
<template>
  <UserMenu />
  <ThemeToggle />
</template>

Correct - Consumer (descendant):

<!-- UserMenu.vue -->
<script setup>
import { inject } from 'vue'

// Inject from any ancestor
const user = inject('user')
</script>

<template>
  <div>{{ user.name }}</div>
</template>
<!-- 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:

// keys.js
export const UserKey = Symbol('user')
export const ThemeKey = Symbol('theme')
<script setup>
import { provide } from 'vue'
import { UserKey, ThemeKey } from './keys'

provide(UserKey, user)
provide(ThemeKey, theme)
</script>
<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:

<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:

<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

<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