Component Communication in Vue: Choosing the Right Data Flow

Every Vue application beyond a single-file demo eventually needs components to share data. The framework offers several mechanisms for this, and picking the correct one depends on the relationship between the components and the direction of the data flow. Below are three core strategies covering parent-to-child, child-to-parent, and application-wide communication.

Props: Passing Data Down the Tree

Props are custom attributes registered on a child component. A parent passes data by binding a value to the prop in its template, and the child declares which props it expects. This creates a one-way data flow from parent to child.

For a profile page with a child component, we can declare a username prop in the child and bind it from the parent.

// AccountInfo.vue

<template>
 <div id='account-info'>
   {{username}}
 </div>
</template>
 
<script>
export default {
 props: ['username']
}
</script>
// ProfilePage.vue
 
<account-info username='matt' />

To bind a dynamic value or a variable from the parent's data, use the v-bind directive (shorthand :) instead of a static string.

<template>
 <div>
   <account-info :username="user.username" />
 </div>
</template>
 
<script>
import AccountInfo from "@/components/AccountInfo.vue";
 
export default {
 components: {
   AccountInfo
 },
 data() {
   return {
     user: {
       username: 'matt'
     }
   }
 }
}
</script>

When the parent's data changes, the child's prop will update accordingly.

Prop Validation and Naming

For clarity in larger projects, define validation rules for each prop. Specifying the expected type, required status, and other conditions lets Vue issue warnings if a prop is used incorrectly.

export default {
 props: {
   username: String
 }
}

Follow the Vue community style guide when naming props: use camelCase in JavaScript logic and kebab-case in HTML templates. Vue automatically converts between the two, so no extra configuration is required.

// GOOD
<account-info :my-username="user.username" />
props: {
   myUsername: String
}
 
// BAD
<account-info :myUsername="user.username" />
props: {
   "my-username": String
}

Custom Events: Communicating Upward

To send data from a child back to its parent, dispatch a custom event using the .​$emit(eventName) method. The parent listens for that event with the v-on directive (shorthand @) and runs a handler.

Consider a button inside a child component that should update a username stored in the parent. The child emits an event, and the parent listens and updates its own data.

<template>
 <div id='account-info'>
   <button @click='changeUsername()'>Change Username</button>
   {{username}}
 </div>
</template>
 
<script>
export default {
 props: {
   username: String
 },
 methods: {
   changeUsername() {
     this.$emit('changeUsername')
   }
 }
}
</script>
<template>
 <div>
   <account-info :username="user.username" @changeUsername="user.username = 'new name'"/>
 </div>
</template>

Events can include arguments. The $emit method accepts an optional second parameter, which is passed to the parent's handler. In the parent, you can access that value inline using the special $event variable or via a method parameter.

this.$emit('changeUsername', 'mattmaribojoc')
<account-info :username="user.username" @changeUsername="user.username = $event"/>
 
OR 
 
<account-info :username="user.username" @changeUsername="changeUsername($event)"/>
 
export default {
...
methods: {
   changeUsername (username) {
     this.user.username = username;
   }
}
}

Never directly mutate a prop from within a component. Instead, emit an event and let the parent change its own data.

Vuex: Centralized State for Any Component

Props and events handle direct parent-child relationships, but they become unwieldy when unrelated components need the same data. Vuex solves this by extracting shared data into a single store that every component can access.

Vuex is a separate package. Install it and create a store module, then register it on the root Vue instance so it is injected into all child components.

// store/index.js
 
import Vue from "vue";
import Vuex from "vuex";
 
Vue.use(Vuex);
 
export default new Vuex.Store({
 state: {},
 getters: {},
 mutations: {},
 actions: {}
});
// main.js
 
import store from "./store";
 
new Vue({
  store,
  ...

Components access the store via this.​$store. The store is structured around four concepts:

State

The state object holds application-level data. Any component can read it directly, but should never change it except through mutations.

export default new Vuex.Store({
 state: {
   user: {
     username: 'matt',
     fullName: 'Matt Maribojoc'
   }
 },
 getters: {},
 mutations: {},
 actions: {}
});
mounted () {
   console.log(this.$store.state.user.username);
},

Getters

Getters return derived values from the state, similar to computed properties. They cache results and only re-evaluate when their dependencies change.

getters: {
   firstName: state => {
     return state.user.fullName.split(' ')[0]
   }
 }

Getter results are available on the store.getters object.

mounted () {
   console.log(this.$store.getters.firstName);
}

By default, getters receive two arguments in their declaration function: state and getters. The state argument is required; getters lets you call other getters.

lastName (state, getters) {
     return state.user.fullName.replace(getters.firstName, '');
}

To pass custom parameters to a getter, make the getter return a function. This allows for flexible lookups, but it disables caching.

prefixedName: (state, getters) => (prefix) => {
     return prefix + getters.lastName;
}
 
// in our component
console.log(this.$store.getters.prefixedName("Mr."));

Mutations

Mutations are the only way to modify state, and they must be synchronous. They take state as the first argument and an optional payload as the second.

mutations: {
   changeName (state, payload) {
     state.user.fullName = payload
   }
},

Call a mutation from a component with store.commit.

this.$store.commit("changeName", "New Name");

Payloads are commonly objects to allow multiple values and improve readability.

changeName (state, payload) {
     state.user.fullName = payload.newName
}

There are two equivalent call styles for committing a mutation: passing the type and payload as separate arguments, or passing a single object containing both.

this.$store.commit("changeName", {
       newName: "New Name 1",
});
 
     // or
 
 this.$store.commit({
       type: "changeName",
       newName: "New Name 2"
});

Actions

Actions handle asynchronous logic and then commit mutations. Unlike mutations, actions can be asynchronous and do not change state directly. They receive a context object that provides access to the store's state, getters, and commit method.

actions: {
   changeName (context, payload) {
     setTimeout(() => {
       context.commit("changeName", payload);
     }, 2000);
   }
}

Dispatch an action from a component using store.dispatch, passing arguments in the same pattern as commit.

this.$store.dispatch("changeName", {
      newName: "New Name from Action"
});

Which Approach Should You Use?

  • Use props when a parent needs to pass data down to a child component.
  • Use custom events when a child needs to notify a parent or send data up the component tree.
  • Use Vuex when data is shared across many components that don't have a direct parent-child relationship, or when you need a single source of truth for application-level state.

For shared state in larger applications, Vuex enforces a clear data flow: components dispatch actions, actions commit mutations, and mutations update state in a predictable, synchronous manner.