Join the Newsletter!

Exclusive content & updates. No spam.

Skip to content

Solving Prop Drilling in Vue: Modern State Management Strategies

Published: at 

TL;DR: Prop Drilling Solutions at a Glance

Click the toggle button to see interactive diagram animations that demonstrate each concept.


The Hidden Cost of Prop Drilling: A Real-World Scenario

Imagine building a Vue dashboard where the user’s name needs to be displayed in seven nested components. Every intermediate component becomes a middleman for data it doesn’t need. Imagine changing the prop name from userName to displayName. You’d have to update six components to pass along something they don’t use!

Current User: Guest
Parent
Provides username to Child
username: Guest
Child
Passes username to Grandchild
username: Guest
Grandchild
Uses username prop
username: Guest

This is prop drilling – and it creates:


Solution 1: Pinia for Global State Management

When to Use: App-wide state (user data, auth state, cart items)

Current User: Guest
User Store
// stores/user.js
export const useUserStore = defineStore('user', {
username: ref('Guest'),
setUsername: (name) => username.value = name
});
Parent
useUserStore()
username: Guest
Child
useUserStore()
username: Guest
Grandchild
useUserStore()
username: Guest

Implementation:

// stores/user.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
const username = ref(localStorage.getItem('username') || 'Guest');
const isLoggedIn = computed(() => username.value !== 'Guest');
  
function setUsername(newUsername) {
    username.value = newUsername;
    localStorage.setItem('username', newUsername);
}

return {
    username,
    isLoggedIn,
    setUsername
};
});

Component Usage:


<!-- DeeplyNestedComponent.vue -->
<script setup>
import { useUserStore } from '@/stores/user';
const user = useUserStore();
</script>

<template>
  <div class="user-info">
    Welcome, {{ user.username }}!
    <button v-if="!user.isLoggedIn" @click="user.setUsername('John')">
      Log In
    </button>
  </div>
</template>

Pros

⚠️ Cons


Solution 2: Composables for Reusable Logic

When to Use: Shared component logic (user preferences, form state)

Current User: Guest
useUser Composable
const username = ref('Guest');
return { username, setUsername };
Parent
const { username } = useUser()
username: Guest
Child
const { username } = useUser()
username: Guest
Grandchild
const { username } = useUser()
username: Guest

Implementation with TypeScript:


// composables/useUser.ts
import { ref } from 'vue';
const username = ref(localStorage.getItem('username') || 'Guest');

export function useUser() {
  const setUsername = (newUsername: string) => {
    username.value = newUsername;
    localStorage.setItem('username', newUsername);
  };

  return { 
    username,
    setUsername,
  };
}

Component Usage:



<!-- UserProfile.vue -->
<script setup lang="ts">
const { username, setUsername } = useUser();
</script>

<template>
  <div class="user-profile">
    <h2>Welcome, {{ username }}!</h2>
    <button @click="setUsername('John')">
      Update Username
    </button>
  </div>
</template>

Pros

⚠️ Cons

Solution 3: Provide/Inject for Component Tree Scoping

When to Use: Library components or feature-specific user data

Current User: Guest
Parent Provider
import { provide, ref } from 'vue';
const username = ref('Guest');
provide(USER_KEY, username);
Symbol Key
const USER_KEY = Symbol('user');
Parent (Provider)
provide(USER_KEY, username)
username: Guest
Child (No Injection)
Passes username implicitly
Grandchild (Injector)
const username = inject(USER_KEY)
username: Guest

Type-Safe Implementation:



// utilities/user.ts
import type { InjectionKey } from 'vue';

interface UserContext {
  username: Ref<string>;
  updateUsername: (name: string) => void;
}

export const UserKey = Symbol('user') as InjectionKey<UserContext>;

// ParentComponent.vue
<script setup lang="ts">
import { UserKey } from '@/utilities/user';

const username = ref<string>('Guest');
const updateUsername = (name: string) => {
  username.value = name;
};

provide(UserKey, { username, updateUsername });
</script>

// DeepChildComponent.vue
<script setup lang="ts">
import { UserKey } from '@/utilities/user';

const { username, updateUsername } = inject(UserKey, {
  username: ref('Guest'),
  updateUsername: () => console.warn('No user provider!'),
});
</script>

Pros

⚠️ Cons


Why Event Buses Fail for State Management

Current User: Guest
Event Bus (mitt)
const emitter = mitt();
// Events flying everywhere!
emitter.emit('userChange', username);
emitter.on('userChange', (name) => {...});
Header
emitter.emit('userChange', 'John')
username: Guest
Settings
emitter.on('userChange', updateUI)
username: Guest

Event buses create more problems than they solve for state management:

  1. Spaghetti Data Flow
    Components become invisibly coupled through arbitrary events. When ComponentA emits update-theme, who’s listening? Why? DevTools can’t help you track the chaos.

  2. State Inconsistencies
    Multiple components listening to the same event often maintain duplicate state:

    // Two components, two sources of truth
    eventBus.on('login', () => this.isLoggedIn = true)
    eventBus.on('login', () => this.userStatus = 'active')
  3. Memory Leaks
    Forgotten event listeners in unmounted components keep reacting to events, causing bugs and performance issues.

Where Event Buses Actually Work

Instead of Event Buses: Use Pinia for state, composables for logic, and provide/inject for component trees.

No

Yes

App-wide

Component Tree

Reusable Logic

Need Shared State?

Props/Events

Scope?

Pinia

Provide/Inject

Composables

Decision Guide: Choosing Your Weapon

Pro Tips for State Management Success

  1. Start Simple: Begin with props, graduate to composables
  2. Type Everything: Use TypeScript for stores/injections
  3. Name Wisely: Prefix stores (useUserStore) and injection keys (UserKey)
  4. Monitor Performance: Use Vue DevTools to track reactivity
  5. Test State: Write unit tests for Pinia stores/composables

By mastering these patterns, you’ll write Vue apps that scale gracefully while keeping component relationships clear and maintainable.

Stay Updated!

Subscribe to my newsletter for more TypeScript, Vue, and web dev insights directly in your inbox.

  • Background information about the articles
  • Weekly Summary of all the interesting blog posts that I read
  • Small tips and trick
Subscribe Now

Most Related Posts