963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
6.0 KiB
6.0 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 |
|
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:
MainLayoutandSidebarare cluttered with props they don't use- Adding a new shared value requires updating every component in the chain
- Removing a deeply nested component requires updating all ancestors
- 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