Skip to the solution
MediumFrontend9 min read

What is defineModel() in Vue 3.4+ and how does it replace manual prop + emit?

932 views

The problem

Question presented to candidate: "Our codebase has dozens of components with a modelValue prop, an update:modelValue emit and a writable computed in between. A colleague wants to replace them all with defineModel(). What does defineModel actually generate, what behaves differently from the manual version, and is there anything that could bite us during the migration?"

What a strong answer should cover:

  • defineModel() is a compiler macro (stable since Vue 3.4, experimental in 3.3) that declares the modelValue prop, a modelModifiers prop and the update:modelValue emit, and returns a ref built with useModel().
  • Reading the ref gives the prop; assigning it emits the update event. Named models: defineModel('title').
  • If the parent passes no v-model, the ref works as local state (the manual version cannot do that without extra code).
  • When the parent does bind it, the ref only changes after the parent re-renders, so a read right after assignment returns the old value.
  • A default that the parent does not have leaves parent and child out of sync until the first emit.
  • Options (type, required, default) go to the prop; get/set transform values, which is how custom modifiers are implemented.

Clarifying questions expected:

  • "Do any of these components rely on reading the new value immediately after setting it?"
  • "Are they TypeScript components? Then defineModel<string>() gives the prop type."

Code / implementation expected: Yes. A component using defineModel, and ideally the compiled output or the useModel equivalent.

  • vue-3.4
  • defineModel
  • v-model

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

9 min read Β· 10 sections

Read the code

VueWhat defineModel compiles to: useModel() with a prop and emit, bound and unbound
<script setup>
import { ref, defineComponent, h, useModel } from "vue";

// In a .vue file you would write: const count = defineModel({ type: Number, default: 0 })
// The compiler turns that into exactly this: a prop, its emit, and useModel()
const Counter = defineComponent({
  props: { modelValue: { type: Number, default: 0 }, modelModifiers: {} },
  emits: ["update:modelValue"],
  setup(props) {
    const count = useModel(props, "modelValue");
    return () =>
      h("button", { onClick: () => count.value++ }, `count: ${count.value}`);
  },
});

const total = ref(5);
</script>

<template>
  <!-- bound: the parent owns the value -->
  <Counter class="bound" v-model="total" />
  <p>parent total = {{ total }}</p>

  <!-- not bound: the child keeps a local value instead -->
  <Counter class="local" />
</template>
VuedefineModel in real SFCs: default value, local mode and a custom modifier
<!-- Counter.vue -->
<script setup>
const count = defineModel({ type: Number, default: 1 });
function inc() {
  count.value++;
  console.log(`  [child] right after count.value++ -> count.value = ${count.value}`);
}
</script>
<template><button @click="inc">{{ count }}</button></template>

<!-- TitleInput.vue -->
<script setup>
const [title, modifiers] = defineModel("title", {
  set(value) {
    return modifiers.capitalize ? value.charAt(0).toUpperCase() + value.slice(1) : value;
  },
});
</script>
<template><input class="title" :value="title" @input="title = $event.target.value" /></template>

<!-- App.vue -->
<script setup>
import { ref } from "vue";
import Counter from "./Counter.vue";
import TitleInput from "./TitleInput.vue";
const bound = ref(10);
const unset = ref();
const heading = ref("");
</script>
<template>
  <Counter id="a" v-model="bound" />
  <Counter id="b" />
  <Counter id="c" v-model="unset" />
  <TitleInput v-model:title.capitalize="heading" />
  <p>bound={{ bound }} unset={{ String(unset) }} heading={{ JSON.stringify(heading) }}</p>
</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 33 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 does v-model work on components?

Continue