Vue.js course Β· Module 7: Props & Events

Advanced Communication Patterns

5 min read
In this lesson4

Now that you know all the communication tools in Vue, let's see how to combine them into advanced patterns - like a complete NOVA LAB communication system.

The problem is no longer how to send a signal, but through which channel. The same piece of information can travel through props, an event, v-model, provide or a global state store. A bad choice only shows up over time: data pushed through five floors, a child that quietly changes someone else's state, or a parent rummaging around inside a child. Below you have a map of the channels and the patterns that keep traffic on the station in order.

When to use which pattern?

The table orders the channels from the shortest reach to the widest. Pinia in the last row is Vue's official library for managing global state - you will get to know it at the Mission Database.

PatternWhen to use
PropsPassing data parent β†’ child (1 level)
EventsCommunication child β†’ parent (1 level)
v-modelTwo-way binding (forms, inputs)
Provide/InjectDeep communication (2+ levels)
PiniaGlobal application state

Props and events connect only neighbours, v-model is their pair merged into one notation, provide reaches any descendant in the subtree, and Pinia can be reached from anywhere in the application.

Pattern: Readonly Provide + Actions

A safe pattern - descendants cannot modify state directly. The provider exposes the state through readonly, and next to it a function that is the only one allowed to change that state:

1<!-- Provider -->
2<script setup>
3import { provide, ref, readonly } from 'vue'
4
5const state = ref({ theme: 'dark', lang: 'en' })
6
7provide('state', readonly(state))
8provide('updateState', (key, value) => {
9  state.value[key] = value
10})
11</script>
12
13<!-- Consumer -->
14<script setup>
15import { inject } from 'vue'
16
17const state = inject('state')         // readonly
18const updateState = inject('updateState') // action
19
20updateState('theme', 'light') // OK - via action
21// state.value.theme = 'light' // readonly: console warning, change ignored
22</script>

The assignment on the last line does not throw an exception: Vue prints a console warning that the target is read-only and ignores the change. Every modification therefore goes through updateState, that is through a single place where it is easy to add logging or data checks. The state still has one owner - the provider.

Pattern: defineExpose

Sometimes a parent needs to give a child a command, for example "reset the form". Components using <script setup> are closed by default: a parent that reaches them through a template ref will not see any variable or function. The defineExpose macro opens up selected ones - this is how a component exposes its methods to the parent via ref:

1<!-- Child.vue -->
2<script setup>
3import { ref } from 'vue'
4
5const count = ref(0)
6const reset = () => { count.value = 0 }
7const validate = () => count.value > 0
8
9defineExpose({ reset, validate })
10</script>
11
12<!-- Parent.vue -->
13<script setup>
14import { ref } from 'vue'
15
16const childRef = ref(null)
17
18const handleReset = () => {
19  childRef.value.reset()
20  const isValid = childRef.value.validate()
21}
22</script>
23
24<template>
25  <Child ref="childRef" />
26  <button @click="handleReset">Reset Child</button>
27</template>

The ref="childRef" attribute binds the component to the ref with the same name in the parent's script. The value only appears after mounting - before that childRef.value is null - which is why we call the methods in response to a click. Since Vue 3.5 the documentation recommends writing useTemplateRef('childRef') instead of ref(null), and it works the same way. The parent sees only reset and validate, while count stays private.

Complete Data Flow

In a full application all channels work at the same time. This is what the component tree of a panel can look like, with each connection using a different pattern:

1App (provide: global state)
2  └── Layout
3       β”œβ”€β”€ Header (inject: theme, user)
4       β”‚    └── Avatar (props: user.avatar)
5       β”‚         └── emit('click') β†’ Header
6       └── Content
7            └── Page (props from router)
8                 └── Form (v-model: data)
9                      └── emit('submit') β†’ Page β†’ API

The loop is always the same. The parent keeps state in ref or reactive and passes it down in props, and the child displays the data read-only. The child may have its own working state, for example a draft typed into a form, but that is still its local business. When it wants to change the parent's data, it emits an event; the parent updates the state in the handler, and Vue re-renders everything that uses that state. An intermediate component such as Layout can at the same time inject what App broadcasts and provide its own data to its subtree. The props for Page, in turn, can be supplied by the router - you will see that at the Navigation Center.

My advice: choose the nearest channel that is sufficient. Treat defineExpose as a service port - for methods like reset() or focus(), not as the main data route. In the editor below AlertPanel changes the alert level only through injected actions. In this location's project all these patterns will work together in one system.

Remember: the best communication on the station is the shortest route that delivers the signal - and always one owner of the data.

Code for this lesson: App.vue
1<script setup>
2import { ref, provide, readonly } from 'vue'
3import Dashboard from './Dashboard.vue'
4
5// Advanced communication patterns
6const stationStatus = ref('operational')
7const alertLevel = ref(0)
8
9// Provide with readonly + actions pattern
10provide('station', readonly(stationStatus))
11provide('alertLevel', readonly(alertLevel))
12provide('raiseAlert', () => {
13  alertLevel.value = Math.min(alertLevel.value + 1, 5)
14})
15provide('clearAlert', () => {
16  alertLevel.value = 0
17})
18</script>
19
20<template>
21  <div class="nova-lab">
22    <h1>Advanced Patterns</h1>
23    <p>Station: {{ stationStatus }} | Alert: {{ alertLevel }}/5</p>
24    <Dashboard />
25  </div>
26</template>
27
28<style scoped>
29.nova-lab {
30  background: #0a0e27;
31  color: #00ff88;
32  min-height: 100vh;
33  padding: 20px;
34  font-family: monospace;
35}
36h1 { color: #00ff88; margin-bottom: 15px; }
37</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. How do you disable automatic attribute inheritance?

  2. 2. How do you access attrs in <script setup>?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange communication patterns from closest to furthest:

  • Horizontal ordering

    Arrange the multiple v-model syntax:

  • Click in order

    Arrange the typed provide with InjectionKey:

  • Vertical ordering

    Arrange the one-way data flow in Vue:

  • Horizontal ordering

    Arrange the defineEmits syntax with payload validation:

  • Click in order

    Arrange the v-model update cycle on a component:

  • Horizontal ordering

    Arrange the code for global provide in main.ts:

  • Click in order

    Arrange the readonly provide pattern:

  • Vertical ordering

    Arrange the full data flow in a Vue application:

  • Vertical ordering

    Arrange the communication tree from top (app) to bottom (leaf):

  • Horizontal ordering

    Arrange the defineExpose syntax:

Useful articles