Component Communication
Vue.js Development
Component Communication
As Vue applications grow, managing how data flows between components becomes critical. Vue follows a unidirectional data flow model: parents pass data to children through props, and children notify parents of changes through emitted events. For deeply nested component trees where prop drilling becomes cumbersome, Vue provides the provide/inject mechanism to pass data through multiple levels without intermediate components needing to know about it. Understanding these communication patterns is essential for building maintainable, scalable Vue applications.
Props Down, Events Up
The fundamental communication pattern in Vue is props flowing down from parent to child and events bubbling up from child to parent. This creates a clear, traceable data flow.
<!-- TodoItem.vue (Child) -->
<template>
<div class="todo-item" :class="{ completed: todo.done }">
<input
type="checkbox"
:checked="todo.done"
@change="$emit('toggle', todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="$emit('delete', todo.id)">Delete</button>
</div>
</template>
<script>
export default {
props: {
todo: { type: Object, required: true },
},
emits: ["toggle", "delete"],
};
</script>
<!-- TodoList.vue (Parent) -->
<template>
<div>
<h2>Todo List ({{ remaining }} remaining)</h2>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add todo..." />
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@delete="deleteTodo"
/>
</div>
</template>
<script>
import TodoItem from "./TodoItem.vue";
export default {
components: { TodoItem },
data() {
return {
newTodo: "",
nextId: 4,
todos: [
{ id: 1, text: "Learn Vue", done: true },
{ id: 2, text: "Build an app", done: false },
{ id: 3, text: "Deploy to production", done: false },
],
};
},
computed: {
remaining() {
return this.todos.filter((t) => !t.done).length;
},
},
methods: {
addTodo() {
if (!this.newTodo.trim()) return;
this.todos.push({ id: this.nextId++, text: this.newTodo, done: false });
this.newTodo = "";
},
toggleTodo(id) {
const todo = this.todos.find((t) => t.id === id);
if (todo) todo.done = !todo.done;
},
deleteTodo(id) {
this.todos = this.todos.filter((t) => t.id !== id);
},
},
};
</script>
Provide and Inject
When data needs to reach deeply nested components, passing props through every intermediate level is tedious. The provide/inject pattern lets an ancestor component provide data that any descendant can inject, regardless of depth.
<!-- App.vue (Provider) -->
<script>
export default {
provide() {
return {
theme: this.theme,
currentUser: this.currentUser,
updateTheme: this.updateTheme,
};
},
data() {
return {
theme: "light",
currentUser: { id: 1, name: "Alice", role: "admin" },
};
},
methods: {
updateTheme(newTheme) {
this.theme = newTheme;
},
},
};
</script>
<!-- DeepChild.vue (Consumer — any depth) -->
<template>
<div :class="`theme-${theme}`">
<p>Logged in as: {{ currentUser.name }}</p>
<button @click="updateTheme('dark')">Dark Mode</button>
</div>
</template>
<script>
export default {
inject: ["theme", "currentUser", "updateTheme"],
};
</script>
v-model on Components
The v-model directive can be used on custom components to create two-way data bindings. Under the hood, it is syntactic sugar for a prop and an emitted event.
<!-- CustomInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="custom-input"
/>
</template>
<script>
export default {
props: {
modelValue: { type: String, default: "" },
},
emits: ["update:modelValue"],
};
</script>
<!-- Parent usage -->
<template>
<div>
<CustomInput v-model="searchQuery" />
<p>Searching: {{ searchQuery }}</p>
</div>
</template>
Tip: Use props and events for direct parent-child communication, provide/inject for deep nesting within a component subtree, and a state management library like Pinia for truly global state that many unrelated components need to access.
Key Takeaways
- Props flow data down from parent to child; events flow notifications up from child to parent.
- Never mutate props directly in a child — emit an event asking the parent to change the data.
- Provide/inject passes data through deeply nested component trees without prop drilling.
v-modelon components creates two-way binding using amodelValueprop andupdate:modelValueevent.- For complex global state, use a dedicated state management library like Pinia.