Skip to content

Is emit synchronous?

TLDR

Events are synchronous, prop updates are not.

Events and listeners

Emitting a component event is synchronous. Any listeners will be called immediately, before any code that follows the emit call.

For example:

js
emit('close')
console.log('event emitted')

Any listeners for this event will run before the console logging occurs. Here's a Playground to demonstrate that:

The same applies if you're using $emit to emit the event.

v-model and defineModel are implemented using component events. Assigning a value to the ref created by defineModel will synchronously emit the corresponding event. The listener created by v-model will also update the parent data synchronously.

Here's a Playground showing this synchronicity with v-model:

But while events are synchronous, prop updates are not, which can make it appear as though the event is deferred in some way.

Prop updates

A component's props are updated when its parent re-renders.

While the event and data update shown above are synchronous, the rendering update is not. Rendering updates are batched (using a microtask), meaning that they don't occur synchronously when reactive data is modified.

Consider this code for parent and child components:

vue
<script setup>
import { ref } from 'vue'
import MyChild from './MyChild.vue'

const parentCount = ref(0)

function onIncrement() {
  parentCount.value++
  console.log('parent count is now ' + parentCount.value)
}
</script>

<template>
  <MyChild :count="parentCount" @increment="onIncrement" />
</template>
vue
<script setup>
const props = defineProps({ count: Number })
const emit = defineEmits(['increment'])

function onButtonClick() {
  emit('increment')
  console.log('props.count: ' + props.count)
}
</script>

<template>
  {{ count }}
  <button @click="onButtonClick">
    Increment
  </button>
</template>

In particular, note these two lines in the child:

js
emit('increment')
console.log('props.count: ' + props.count)

The first line emits an event and the listener in the parent updates parentCount. All synchronous. Modifying the reactive data triggers the re-rendering of the parent component, but that doesn't happen immediately, it just gets added to a queue. That queue won't be processed until other code has finished, so props.count won't be updated yet and the console logging will show the old value.

If you need to use the new value of the prop, you can wait using nextTick:

js
emit('increment')
await nextTick()
console.log('props.count: ' + props.count)

The promise returned by nextTick will resolve after rendering is complete, so the props will all be updated to their new values.

Alternatively, you may be able to use a watcher on the prop to react when the prop's value changes, but that will be triggered by all changes to the prop's value, not just changes resulting from the emit call.

v-model/defineModel updates

We see the same thing with v-model and defineModel:

js
const model = defineModel()
model.value = 7
console.log(model.value)

While you might expect this to log 7, it'll actually log the old value of the prop, assuming one exists. In cases where the parent doesn't use v-model (or equivalent), defineModel will fallback to 'local mode', where it manages the value itself. In that case it will update immediately.

Here are some Playgrounds that demonstrate this:

While it may appear unintuitive that the model.value property doesn't update synchronously, it's important to keep the underlying mechanism in mind when working with defineModel/v-model. It's a shorthand for working with a prop/event pair and props don't update synchronously.

While defineModel could be implemented to always update the child's local ref synchronously, that leads to other problems. The child is not the owner of the data, it's being passed in by the parent. When the child emits an event, it's asking the parent to update the prop to some new value. It's for the parent to decide how to react to that event. It might choose to reject the update, or to use a different value. In general, the child shouldn't assume that the value it emits will be respected by the parent.

As with other props, we could use nextTick or a watcher to wait for the model to update.