Vue.js communication Part 2: parent-child components
5 min read

Vue.js communication Part 2: parent-child components

Many Vue.js app components will probably have some parent-child relationship. In this part of the series, I aim to talk about common patterns and 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 independent of the parent.

This article is the second article of a series of Vue.js communication articles.

Outline

  • Vue.js communication: single components
  • Vue.js communication: parent-child components
  • 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 depends on having a specific parent, 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.

<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 example 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 to make the child dependent on the parent that is no longer obvious but can still lead to issues.

Here is a real-life implementation that I have 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 actual implementation was a lot more complex since it made a lot of decisions based on the isHomepage flag.

This approach has a problem because the component makes assumptions about the parent who 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, you'll gain more control over 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 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 straightforward. 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>

You can fall into a trap 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 not be able to understand where changes are coming from.

Don't do this unless you have an excellent 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 in part 1.

There aren't too many cases I can think of where I would want to use refs.

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 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  and show . When the modal sets show  to false, then the parent's visible  variable might still be true.

Way up: Child to parent

There are two ways how to pass data to the parent. Events and functions via props.

Events pass data up the tree.

Functions via props can be used to pass data and execute some work, such as painting on a canvas and starting a file upload.

Events

Events are the standard way how to pass data up the tree. You can define events you like and give any data. 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.

Vue events don't bubble up the component tree when planning your data flow. 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 a Vue event to a native event.

Functions via props

You can pass functions via props if you want. These functions will be bound to the parent's Vue instance.

I don't use this technique. I would instead use events because the child component that should run a function will have to check if the function is present to prevent a run time error. But also because I don't see a real benefit yet. It would be great to see some good examples.

In some instances, 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 components 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 of making it work for a text input component.

The child emits input events.

@input="$emit('input', $event.target.value)"
The parent listens to 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"

v-model does this. 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, use props and events.

Also, look at the fantastic guides to see detailed options and advanced options you have.

Further reading