Disclaimer: this answer was written a long time ago and it may not reflect latest Vue development or trends. Take everything in this answer with a grain of salt and please comment if you find anything that's outdated, no longer valid, or unhelpful.
State scopes
When designing a Vue application (or in fact, any component based application), there are different types of data that depend on which concerns we're dealing with and each has its own preferred communication channels.
Global state: may include the logged in user, the current theme, etc.
Local state: form attributes, disabled button state, etc.
Note that part of the global state might end up in the local state at some point, and it could be passed down to child components as any other local state would, either in full or diluted to match the use-case.
Communication channels
A channel is a loose term I'll be using to refer to concrete implementations to exchange data around a Vue app.
Each implementation addresses a specific communication channel, which includes:
- Global state
- Parent-child
- Child-parent
- Siblings
Different concerns relate to different communication channels.
Props: Direct Parent-Child
The simplest communication channel in Vue for one-way data binding.
Events: Direct Child-Parent
Important notice: $on
and $once
were removed in Vue version 3.
$emit
and v-on
event listeners. The simplest communication channel for direct Child-Parent communication. Events enable 2-way data binding.
Provide/Inject: Global or distant local state
Added in Vue 2.2+, and really similar to React's context API, this could be used as a viable replacement to an event bus.
At any point within the components tree could a component provide some data, which any child down the line could access through the inject
component's property.
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
}
})
This could be used to provide global state at the root of the app, or localized state within a subset of the tree.
Centralized store (Global state)
Note: Vuex 5 is going to be Pinia apparently. Stay tuned. (Tweet)
Vuex is a state management pattern + library for Vue.js applications.
It serves as a centralized store for all the components in an
application, with rules ensuring that the state can only be mutated in
a predictable fashion.
And now you ask:
[S]hould I create vuex store for each minor communication?
It really shines when dealing with global state, which includes but is not limited to:
- data received from a backend,
- global UI state like a theme,
- any data persistence layer, e.g. saving to a backend or interfacing with local storage,
- toast messages or notifications,
- etc.
So your components can really focus on the things they're meant to be, managing user interfaces, while the global store can manage/use general business logic and offer a clear API through getters and actions.
It doesn't mean that you can't use it for component logic, but I would personally scope that logic to a namespaced Vuex module with only the necessary global UI state.
To avoid dealing with a big mess of everything in a global state, see the Application structure recommandations.
Refs and methods: Edge cases
Despite the existence of props and events, sometimes you might still
need to directly access a child component in JavaScript.
It is only meant as an escape hatch for direct child manipulation -
you should avoid accessing $refs
from within templates or computed properties.
If you find yourself using refs and child methods quite often, it's probably time to lift the state up or consider the other ways described here or in the other answers.
Similar to $root
, the $parent
property can be used to access the
parent instance from a child. This can be tempting to reach for as a
lazy alternative to passing data with a prop.
In most cases, reaching into the parent makes your application more
difficult to debug and understand, especially if you mutate data in
the parent. When looking at that component later, it will be very
difficult to figure out where that mutation came from.
You could in fact navigate the whole tree structure using $parent
, $ref
or $root
, but it would be akin to having everything global and likely become unmaintainable spaghetti.
Event bus: Global/distant local state
See @AlexMA's answer for up-to-date information about the event bus pattern.
This was the pattern in the past to pass props all over the place from far up down to deeply nested children components, with almost no other components needing these in between. Use sparingly for carefully selected data.
Be careful: Subsequent creation of components that are binding themselves to the event bus will be bound more than once--leading to multiple handlers triggered and leaks. I personally never felt the need for an event bus in all the single page apps I've designed in the past.
The following demonstrates how a simple mistake leads to a leak where the Item
component still triggers even if removed from the DOM.
// A component that binds to a custom 'update' event.
var Item = {
template: `<li>{{text}}</li>`,
props: {
text: Number
},
mounted() {
this.$root.$on('update', () => {
console.log(this.text, 'is still alive');
});
},
};
// Component that emits events
var List = new Vue({
el: '#app',
components: {
Item
},
data: {
items: [1, 2, 3, 4]
},
updated() {
this.$root.$emit('update');
},
methods: {
onRemove() {
console.log('slice');
this.items = this.items.slice(0, -1);
}
}
});
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<div id="app">
<button type="button" @click="onRemove">Remove</button>
<ul>
<item v-for="item in items" :key="item" :text="item"></item>
</ul>
</div>
Remember to remove listeners in the destroyed
lifecycle hook.
Component types
Disclaimer: the following "containers" versus "presentational" components is just one way to structure a project and there are now multiple alternatives, like the new Composition API that could effectively replace the "app specific containers" I'm describing below.
To orchestrates all these communications, to ease re-usability and testing, we could think of components as two different types.
- App specific containers
- Generic/presentational components
Again, it doesn't mean that a generic component should be reused or that an app specific container can't be reused, but they have different responsibilities.
App specific containers
Note: see the new Composition API as an alternative to these containers.
These are just simple Vue component that wraps other Vue components (generic or other app specific containers). This is where the Vuex store communication should happen and this container should communicate through other simpler means like props and event listeners.
These containers could even have no native DOM elements at all and let the generic components deal with the templating and user interactions.
scope somehow events
or stores
visibility for siblings components
This is where the scoping happens. Most components don't know about the store and this component should (mostly) use one namespaced store module with a limited set of getters
and actions
applied with the provided Vuex binding helpers.
Generic/presentational components
These should receive their data from props, make changes on their own local data, and emit simple events. Most of the time, they should not know a Vuex store exists at all.
They could also be called containers as their sole responsibility could be to dispatch to other UI components.
Sibling communication
So, after all this, how should we communicate between two sibling components?
It's easier to understand with an example: say we have an input box and its data should be shared across the app (siblings at different places in the tree) and persisted with a backend.
❌ Mixing concerns
Starting with the worst case scenario, our component would mix presentation and business logic.
// MyInput.vue
<template>
<div class="my-input">
<label>Data</label>
<input type="text"
:value="value"
:input="onChange($event.target.value)">
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
value: "",
};
},
mounted() {
this.$root.$on('sync', data => {
this.value = data.myServerValue;
});
},
methods: {
onChange(value) {
this.value = value;
axios.post('http://example.com/api/update', {
myServerValue: value
});
}
}
}
</script>
While it might look fine for a simple app, it comes with a lot of drawbacks:
- Explicitly uses the global axios instance
- Hard-coded API inside the UI
- Tightly coupled to the root component (event bus pattern)
- Harder to do unit tests
✅ Separation of concerns
To separate these two concerns, we should wrap our component in an app specific container and keep the presentation logic into our generic input component.
With the following pattern, we can:
- Easily test each concern with unit tests
- Change the API without impacting components at all
- Configure HTTP communications however you'd like (axios, fetch, adding middlewares, tests, etc)
- Reuse the input component anywhere (reduced coupling)
- React to state changes from anywhere in the app through the global store bindings
- etc.
Our input component is now reusable and doesn't know about the backend nor the siblings.
// MyInput.vue
// the template is the same as above
<script>
export default {
props: {
initial: {
type: String,
default: ""
}
},
data() {
return {
value: this.initial,
};
},
methods: {
onChange(value) {
this.value = value;
this.$emit('change', value);
}
}
}
</script>
Our app specific container can now be the bridge between the business logic and the presentation communication.
// MyAppCard.vue
<template>
<div class="container">
<card-body>
<my-input :initial="serverValue" @change="updateState"></my-input>
<my-input :initial="otherValue" @change="updateState"></my-input>
</card-body>
<card-footer>
<my-button :disabled="!serverValue || !otherValue"
@click="saveState"></my-button>
</card-footer>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { NS, ACTIONS, GETTERS } from '@/store/modules/api';
import { MyButton, MyInput } from './components';
export default {
components: {
MyInput,
MyButton,
},
computed: mapGetters(NS, [
GETTERS.serverValue,
GETTERS.otherValue,
]),
methods: mapActions(NS, [
ACTIONS.updateState,
ACTIONS.saveState,
])
}
</script>
Since the Vuex store actions deal with the backend communication, our container here doesn't need to know about axios and the backend.
$emit
combined withv-model
to emulate.sync
. i think you should go the Vuex way – Nightshade