Vue.js communication Part 2: parent-child components
Many Vue.js app components will probably have some sort of parent-child relationship. In this part of the series I aim to talk about common patterns as well as anti-patterns that you should avoid. If you follow the basic structure you should be safe for the future. The goal is to understand how to write reusable components that are independent from the parent.
This is the second article of a series of Vue.js communication articles.
Outline
- Vue.js communication: single components
- Vue.js communication: parent-child components
- TODO: Vue.js communication: any component (using vuex)
Rules
Those are the most important rules:
- Parents are allowed to reference children, e.g. via props or refs
- Children do not have any reference to the parent
- Children do not change data that is passed via props.
Important: If the child is dependent on having a specific parent, then you cannot reuse the child component in other situations.
Anti-patterns
I'm going to start with some anti-patterns to set up the scene and make you aware of problems that might occur if you don't follow the rules.
This will be obvious to many but I'm still going to demonstrate it.
<template>
<div class="username-text-input">
<input
@input="$event => { $parent.username = $event.target.value }"
value="$parent.username"
>
</div>
</template>
<script>
export default {
name: 'UsernameTextInput'
}
</script>
This will work if the parent has a variable username
declared. But you couldn't reuse it for other variables than username
.
There are other ways how to make the child dependent on the parent that are not that obvious anymore but can still lead to issues.
Here is a real life implementation that I have actually seen in a project. It seems like it's decoupled, because it uses props and doesn't reference $parent
. But its implementation still makes assumptions about the environment it is being used in.
<template>
<div class="teaser">
<img :src="imageUrl">
<div
class="teaser__title"
:class="{ "teaser__title--inset": this.isHomepage }"
>
{{ title }}
</div>
</div>
</template>
<script>
export default {
name: 'Teaser',
props: {
imageUrl: {
type: String,
required: true
},
isHomepage: {
type: Boolean,
default: () => false
}
}
}
</script>
<style lang="sass" scoped>
.teaser
position: relative
.teaser__title--inset
position: absolute
left: 0
right: 0
bottom: 10px
</style>
The real implementation was actually a lot more complex since it made a lot of decisions based on the isHomepage
flag.
This approach has a problem because the component is making assumptions about the parent that uses it.
It is better to think of features and let the parent configure its use case, e.g. the teaser component could have props like isTitleInset
or titleLocation
('inside' or 'outside').
You might want to add additional features such as the title color or a set of hover animations. If you split these up into more props then you'll gain more control for current and future use cases.
Always ask yourself: Is this component independent from the parent? Is it making assumptions about the parent?
Way down: Parent to child
Props
This is the most common way how to pass data down to a child. Just use a prop. There are different types of props you can pass, e.g. Number
, String
, Object
, Function
.
Numbers and strings are straight forward. Just pass them down.
<template>
<div class="loader">
<div class="loader__bar" style="{ width: `${percent}%` }" />
</div>
</template>
<script>
export default {
name: 'Loader',
props: {
percent: {
type: Number
required: true
}
}
}
</script>
There is a trap you can fall into, when passing down Object
. Make sure the child component doesn't change properties on that object. It's something I've seen in a real-life project as well.
Especially if you make it a habit to let the child change properties on an object you might end up not being able to understand where changes are coming from.
Don't do this unless you have a really good reason:
<template>
<div class="user">
<input type="text" v-model="user.username">
<input type="text" v-model="user.email">
<input type="text" v-model="user.password">
</div>
</template>
<script>
export default {
name: 'User',
props: {
user: {
type: Object,
required: true
}
}
}
</script>
Refs
In most cases you will use refs to reference DOM elements instead of vue components as we did on part 1 of this series. There aren't too many cases I can think of where I would want to use refs. But that might be taste.
I've seen a few vue components that provide public methods, e.g. for modals.
export default {
props: {
// ...
visible: Boolean
},
data () {
return {
show: this.visible
}
},
methods: {
// ...
active () {
this.show = true
},
deactive () {
if(this.closable)
this.show = false
}
}
}
See on github: vue-admin's modal implementation
To use the modal you would have to do this:
this.$refs.myModal.active()
I personally would have tried to aim for props in that case. I wouldn't add the extra complexity to provide methods and props for the same thing.
In this implementation there is a disconnect with visible</span> and
show. When the modal sets
showto
false, then the parent's
visiblevariable might still be
true` .
Way up: Child to parent
There are two way how to pass data to the parent. Events and functions via props. Events pass data up the tree. Functions via props can be used for both passing data but also just to execute some work, such as painting on a canvas, start a file upload.
Events
Events are the standard way how to pass data up the tree. You can define events you like and pass any kind of data with it. The most common way how to listen to an event is to use v-on
or @
in your template when placing a child component. You can also use a ref like this
this.$refs.myChildComponentRef.$on('click', this.handleClick)
Note that there are two types of events. Native events and vue events. Depending on where you place the listener you might get a vue event object or a native one.
The main difference when planning your data flow is that vue events don't bubble up the component tree. Every component in a nested tree needs to pass along the event by re-emitting it.
You're going to receive a vue event when attaching a listener using v-on
or @
to a child component that uses this.$emit
to emit an event. If you still like to receive the native event then you need to add the native modifier like v-on:click.native
or @click.native
This example demonstrates how to change from vue event to native events.
Functions via props
You can pass functions via props if you want. These functions will be bound to the parent's vue instance. I personally don't use this technique. I would rather use events. Just because the child component that should run a function will have to check if the function is present in order to prevent a run time error. But also because I don't see a real benefit, yet. Would be great to see some good examples.
In certain cases it might make sense to use them though, e.g. if you're building a paint application and have a toolbar of buttons.
Two way: v-model
In some cases parent and child component have to communicate two-way. That's just as easy as combining what we've discussed before.
On the way down from the parent: change a child's prop. On the way up from the child: emits an event that the parent receives.
This is an example how to make it work for a text input component.
The child emits input events
@input="$emit('input', $event.target.value)"
The parent listens to the those events and sets the new value to firstName
@input="value => { this.firstName = value }"
The child receives back the new value using the prop
:value="value"
This is exactly what v-model
does. I've replaced the parent's template with v-model
Final words
Parent child communication is going to be easy if you follow the rules.
When in doubt just use props and events.
Also have a look at the awesome guides to see detailed options and advanced options you have.
Further reading