Skip to the solution
MediumFrontend11 min read

How do Vue components communicate?

477 views

The problem

Question presented to candidate: "We have a checkout page with a cart summary, a quantity stepper inside each line item, and a badge in the top toolbar that shows the item count. Walk me through the ways Vue components can share data and send messages, and which one you would use for each of those pieces."

What a strong answer should cover:

  • Props down, events up is the default: the parent passes data with props, the child reports changes with emit, and the child never mutates a prop.
  • v-model on a component is shorthand for a modelValue prop plus an update:modelValue event, and defineModel() (Vue 3.4+) writes both for you.
  • Slots let a parent send template content into a child, and scoped slots let the child pass data back into that content.
  • provide / inject skips the layers in between for deep trees (theme, form context, a cart), and providing a ref keeps it reactive.
  • Template refs + defineExpose for the rare imperative call (focus, reset), and a store such as Pinia for state shared by distant parts of the app.
  • Vue 3 removed $on / $off, so the Vue 2 "event bus" needs an external emitter, and is usually a sign a store or provide/inject fits better.

Clarifying questions expected:

  • "Are these components parent and child, or far apart in the tree?"
  • "Does the shared state need to survive route changes or be visible in devtools?"

Code / implementation expected: Yes β€” a child that takes a prop and emits an event (or uses defineModel), plus a provide/inject or store example for the distant badge.

  • props
  • events

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 9 sections

Read the code

VueProps down, events up, v-model and provide/inject in one app
<script setup>
import { ref, computed, provide, inject, defineComponent, h } from "vue";

// Child -> parent: props come down, events go up.
const Stepper = defineComponent({
  props: { modelValue: Number, step: { type: Number, default: 1 } },
  emits: ["update:modelValue", "limit"],
  setup(props, { emit }) {
    const bump = (dir) => {
      const next = props.modelValue + dir * props.step;
      if (next < 0 || next > 10) return emit("limit", next);
      emit("update:modelValue", next);
    };
    return () =>
      h("span", { class: "stepper" }, [
        h("button", { class: "minus", onClick: () => bump(-1) }, "-"),
        ` ${props.modelValue} `,
        h("button", { class: "plus", onClick: () => bump(1) }, "+"),
      ]);
  },
});

// Deep descendant: reads shared state without the middle layer passing props.
const Badge = defineComponent({
  setup() {
    const cart = inject("cart");
    return () => h("strong", { class: "badge" }, `badge sees ${cart.total.value} items`);
  },
});
const Toolbar = defineComponent({
  setup() {
    return () => h("nav", [h("span", "toolbar | "), h(Badge)]);
  },
});

const apples = ref(2);
const pears = ref(1);
const warning = ref("");
const total = computed(() => apples.value + pears.value);
provide("cart", { total });
</script>

<template>
  <p>apples <Stepper v-model="apples" :step="2" @limit="(v) => (warning = 'apples cannot be ' + v)" /></p>
  <p>pears <Stepper v-model="pears" @limit="(v) => (warning = 'pears cannot be ' + v)" /></p>
  <Toolbar />
  <p class="warn">{{ warning }}</p>
</template>
VueStepper.vue: the same child written with defineModel, defineProps and defineEmits
<script setup>
// Parent to child: props. Child to parent: events. Two-way: defineModel (Vue 3.4+).
const count = defineModel({ type: Number, default: 0 });
const props = defineProps({ step: { type: Number, default: 1 }, max: { type: Number, default: 10 } });
const emit = defineEmits(["limit"]);

function bump(dir) {
  const next = count.value + dir * props.step;
  if (next < 0 || next > props.max) return emit("limit", next);
  count.value = next; // emits update:modelValue for the parent
}
</script>

<template>
  <button @click="bump(-1)">-</button>
  <output>{{ count }}</output>
  <button @click="bump(1)">+</button>
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 53 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

How do you test Vue components (Vitest and Vue Test Utils)?

Continue