Skip to content

Refactoring Prop Drilling with Provide and Inject in Vue 3

In this guide, we'll explore how to refactor prop drilling in a Vue 3 application using the provide and inject API. We'll start with an example that uses local data and prop drilling to pass data through multiple component layers, identify its drawbacks, and then refactor it using provide and inject. We'll also show how to handle fetch requests and share fetched data across components.

What is Prop Drilling?

Prop drilling refers to the process of passing data from a parent component down to deeply nested child components through multiple layers of intermediate components. This can make your code:

  • Hard to maintain.
  • Prone to errors.
  • Cluttered with unnecessary props.

The Problem with Prop Drilling (Using Local Data)

Let's consider a movie app where the App component holds a list of movies (local data), and we need to display this data in a deeply nested MovieItem component.

Initial Setup with Prop Drilling

First, we'll set up our components using local data.

App.vue

src/App.vue
vue
<template>
  <div id="app">
    <MovieList :movies="movies" />
  </div>
</template>

<script setup lang="ts">
import { defineComponent } from 'vue';
import MovieList from './components/MovieList.vue';

const movies = [
  { id: 1, title: 'Inception', description: 'A mind-bending thriller.' },
  { id: 2, title: 'The Matrix', description: 'A cyberpunk classic.' },
  { id: 3, title: 'Interstellar', description: 'A journey through space and time.' },
];
</script>

MovieList.vue

src/components/MovieList.vue
vue
<template>
  <div>
    <h2>Movie List</h2>
    <ul>
      <MovieItem v-for="movie in movies" :key="movie.id" :movie="movie" />
    </ul>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';
import MovieItem from './MovieItem.vue';

const props = defineProps<{
  movies: Array<{ id: number; title: string; description: string }>;
}>();
</script>

MovieItem.vue

src/components/MovieItem.vue
vue
<template>
  <li>
    {{ movie.title }}
  </li>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps<{
  movie: { id: number; title: string; description: string };
}>();
</script>

In this example:

  • App.vue holds the movies data and passes it to MovieList.vue via the movies prop.
  • MovieList.vue receives the movies prop and passes each movie to MovieItem.vue via the movie prop.

Adding a Theme Prop

Now, suppose we want to add a theme (e.g., 'light' or 'dark') that affects the styling of MovieItem.vue. We need to pass the theme prop through each component:

Updated App.vue

src/App.vue
vue
<template>
  <div id="app">
    <MovieList :movies="movies" :theme="theme" />
  </div>
</template>

<script setup lang="ts">
import MovieList from './components/MovieList.vue';

const movies = [
  { id: 1, title: 'Inception', description: 'A mind-bending thriller.' },
  { id: 2, title: 'The Matrix', description: 'A cyberpunk classic.' },
  { id: 3, title: 'Interstellar', description: 'A journey through space and time.' },
];

const theme = 'light';
</script>

Updated MovieList.vue

src/components/MovieList.vue
vue
<template>
  <div>
    <h2>Movie List</h2>
    <ul>
      <MovieItem v-for="movie in movies" :key="movie.id" :movie="movie" :theme="theme" />
    </ul>
  </div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';
import MovieItem from './MovieItem.vue';

const props = defineProps<{
  movies: Array<{ id: number; title: string; description: string }>;
  theme: string;
}>();
</script>

Updated MovieItem.vue

src/components/MovieItem.vue
vue
<template>
  <li :class="theme">
    {{ movie.title }}
  </li>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';

const props = defineProps<{
  movie: { id: number; title: string; description: string };
  theme: string;
}>();
</script>

<style>
.light {
  color: black;
}
.dark {
  color: white;
  background-color: black;
}
</style>

Pitfall

Passing down props through multiple layers can lead to bloated components and increased maintenance overhead.

Refactoring with Provide and Inject (Using Local Data)

We can simplify data sharing by using the provide and inject API, eliminating the need to pass props through every component layer.

Step 1: Provide Data in Ancestor Component

In App.vue, we use the provide function to supply the theme and movies data.

Updated App.vue

src/App.vue
vue
<template>
  <div id="app">
    <MovieList />
  </div>
</template>

<script setup lang="ts">
import { provide } from 'vue';
import MovieList from './components/MovieList.vue';

const movies = [
  { id: 1, title: 'Inception', description: 'A mind-bending thriller.' },
  { id: 2, title: 'The Matrix', description: 'A cyberpunk classic.' },
  { id: 3, title: 'Interstellar', description: 'A journey through space and time.' },
];

const theme = 'light';

provide('movies', movies);
provide('theme', theme);
</script>
  • We remove the :movies and :theme props from MovieList.
  • We provide movies and theme using provide.

Step 2: Remove Unnecessary Props from Intermediate Component

MovieList.vue no longer needs to accept movies and theme as props.

Updated MovieList.vue

src/components/MovieList.vue
vue
<template>
  <div>
    <h2>Movie List</h2>
    <ul>
      <MovieItem v-for="movie in movies" :key="movie.id" :movie="movie" />
    </ul>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue';
import MovieItem from './MovieItem.vue';

const movies = inject<{ id: number; title: string; description: string }[]>('movies');

if (!movies) {
  throw new Error('movies not provided');
}
</script>
  • We inject movies directly using inject('movies').
  • We remove the movies and theme props definitions.

TIP

By injecting movies, we simplify MovieList.vue and avoid unnecessary prop passing.

Step 3: Inject Data in Descendant Component

In MovieItem.vue, we inject theme directly.

Updated MovieItem.vue

src/components/MovieItem.vue
vue
<template>
  <li :class="theme">
    {{ movie.title }}
  </li>
</template>

<script setup lang="ts">
import { defineProps, inject } from 'vue';

const props = defineProps<{
  movie: { id: number; title: string; description: string };
}>();

const theme = inject<string>('theme', 'light');
</script>

<style>
.light {
  color: black;
}
.dark {
  color: white;
  background-color: black;
}
</style>
  • We inject theme using inject('theme', 'light'), with a default value of 'light'.

Do's and Don'ts

  • Do use provide and inject to simplify data sharing across components.
  • Don't overuse provide and inject for data that can be passed via props between closely related components.

Handling Fetch Requests and Sharing Data

Now, let's introduce data fetching into our application. We'll fetch movies data from an API and share it across components.

Step 1: Fetch Data in the Provider Component

In App.vue, we'll fetch movies data and provide it to descendant components.

Updated App.vue (with fetch request)

src/App.vue
vue
<template>
  <div id="app">
    <MovieList />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, provide } from 'vue';
import MovieList from './components/MovieList.vue';

interface Movie {
  id: number;
  title: string;
  description: string;
}

const movies = ref<Movie[]>([]);
const theme = ref('light');

provide('movies', movies);
provide('theme', theme);

onMounted(async () => {
  try {
    const response = await fetch('https://api.example.com/movies');
    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }
    movies.value = await response.json();
  } catch (err) {
    console.error(err);
  }
});
</script>
  • We change movies to a ref<Movie[]>([]) to make it reactive.
  • We fetch movies data in the onMounted lifecycle hook and update movies.value.
  • Since movies is reactive and provided, any component injecting movies will react to changes.

Step 2: Update Components to Handle Reactive Data

Updated MovieList.vue

src/components/MovieList.vue
vue
<template>
  <div>
    <h2>Movie List</h2>
    <ul>
      <MovieItem v-for="movie in movies" :key="movie.id" :movie="movie" />
    </ul>
  </div>
</template>

<script setup lang="ts">
import { inject } from 'vue';
import MovieItem from './MovieItem.vue';

interface Movie {
  id: number;
  title: string;
  description: string;
}

const movies = inject<Ref<Movie[]>>('movies');

if (!movies) {
  throw new Error('movies not provided');
}
</script>
  • Since movies is a ref, we declare its type as Ref<Movie[]>.

Updated MovieItem.vue

No changes needed; movie is passed as a prop from MovieList.vue.

Important

Ensure that reactive data is correctly typed when injected to maintain TypeScript support and prevent runtime errors.

Showing Pitfalls, Do's and Don'ts, Tips, and Important Notes

Pitfalls

Pitfall

Reactive Data Not Updating:

If you inject a reactive ref but destructure it, you lose reactivity.

Solution: Use the injected ref directly without destructuring.

ts
// Incorrect
const movies = inject<Ref<Movie[]>>('movies');
const movieList = movies.value; // This breaks reactivity

// Correct
const movies = inject<Ref<Movie[]>>('movies');

Do's and Don'ts

Do's

  • Do use provide and inject to simplify data sharing across non-parent-child components.
  • Do wrap primitive values with ref when you need reactivity.
  • Do use TypeScript interfaces to define types for your data.

Don'ts

  • Don't overuse provide and inject for simple parent-child prop passing.
  • Don't forget to handle cases where the injected value might be undefined.
  • Don't mutate injected properties directly if they are not reactive.

Tips

TIP

  • Use Symbols for Keys: Using symbols as keys when providing and injecting prevents naming collisions.

    ts
    // Define a symbol
    const MoviesSymbol = Symbol('movies');
    
    // Provide
    provide(MoviesSymbol, movies);
    
    // Inject
    const movies = inject<Ref<Movie[]>>(MoviesSymbol);
  • Default Values: When injecting, you can provide a default value.

    ts
    const theme = inject<string>('theme', 'light');

Important Notes

Important

  • Type Safety with TypeScript: Always specify types when using provide and inject to leverage TypeScript's type checking.

    ts
    // Provide
    provide('movies', movies);
    
    // Inject with type
    const movies = inject<Ref<Movie[]>>('movies');
  • Lifecycle Hooks: Ensure that data is provided before components that inject it are mounted.

Best Practices

1. Use Provide and Inject for Cross-Cutting Concerns

Ideal for data or services that are needed by many components at different levels:

  • Theme settings
  • Localization
  • User authentication status

2. Avoid Overusing Provide and Inject

While powerful, overusing can make your code harder to follow. Use when:

  • Prop drilling becomes unmanageable.
  • The data is truly global or widely used.

3. Keep Reactive Data Reactive

Always wrap your data with ref or reactive when providing if you need reactivity.

4. Handle Injection Failures

Always check if the injected value exists and handle cases where it doesn't.

ts
const value = inject('key');
if (!value) {
  throw new Error('key not provided');
}

Pitfalls

Pitfall 1: Injecting Non-Reactive Data

Issue: Providing a primitive value directly (e.g., a string) doesn't maintain reactivity.

Solution: Wrap primitive values with ref when providing.

ts
// Provide
const theme = ref('light');
provide('theme', theme);

// Inject
const theme = inject<Ref<string>>('theme');

Pitfall 2: Components Rendering Before Provide

Issue: A component tries to inject a value before it's provided.

Solution: Ensure that the provider component is an ancestor in the component tree and is rendered before the injecting component.

Pitfall 3: Overwriting Injected Values

Issue: Accidentally mutating an injected value can cause unexpected behavior.

Solution: Treat injected values as read-only unless intentionally shared for mutation.

Conclusion

By starting with local data and then handling fetch requests, we've demonstrated how to:

  • Refactor prop drilling with provide and inject in Vue 3.
  • Simplify data sharing across components without passing props through every level.
  • Use practical examples to manage themes and shared data.
  • Handle data fetching and share fetched data across components efficiently.
  • Correctly use defineProps with proper TypeScript types.

Remember: Provide and inject are powerful tools that, when used appropriately, can greatly enhance the scalability and cleanliness of your Vue applications.

Happy Coding! If you have any questions or need further assistance, feel free to ask.