Vue's new `defineModel`
Tags: vue, webdev, frontend
Published: 2024-02-01 23:39:42

It was available as a macro for some time, experimental in 3.3, and now stable in 3.4. An amazing feature that shortens your code a lot and prevents you from having to think of a name used inside the component for the `Ref` proxy made with `useVModel` or manual `computed`.

This article was written with Vue v3.4.15, things may change in newer versions.

## What is it and why is it awesome?

Previously, if we wanted to make a prop that would sync its value, we had to write this monstrosity:

```typescript
const props = defineProps<{
  modelValue: Item
}>();

const emit = defineEmits<{
  (e: 'update:modelValue', v: Item): void
}>();

const innerValue = useVModel(props, 'modelValue', emit);
```

And without `useVModel`:

```typescript
const innerValue = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v),
});
```

We have to repeat the name `"modelValue"` 3 times (or 4 without `useVModel`). If we wanted to use it in our template, it would have to have a different name from the prop (that's why I used `innerValue`). Now it all shortens to this:

```typescript
const modelValue = defineModel<Item>();
```

And that's it. 7 generic lines of code were reduced into one. We can use the same name for variable and prop, and the type here will be `Ref<Item|undefined>`, we'll fix it later.

### Set custom name

We can have multiple `v-model` on a component, so each has to have a different name. The default name is `"modelValue"`, but if we want to specify the name of the model, we have to pass it through the first argument:

```typescript
const innerValue = defineModel<Item>("myValue");
```

We can define more than one model that way:

```typescript
const innerValue = defineModel<Item>(); // default modelValue
const viewType = defineModel<ViewType>("viewType");
```

The compiler will warn you if two models have the same name, no worries.

### Set the default value or required

The second (or the first, if we are fine with `"modelValue"` as the name) argument is an object to set up the prop. We can set default value with `default` field, which is either a primitive value, or a function that returns the instance of object (for object types):

```typescript
// when it's object
const innerValue = defineModel<Item>({ default: () => newItem() );
// when it's a primitive
const primitiveValue = defineModel<number>(
  "count", { default: 10 }
);
```

And field `required` to make the field... required, remove the `| undefined` from type:

```typescript
const innerValue = defineModel<Item>({ required: true });
```

And now we have `Ref<Item>` instead of `Ref<Item | undefined>`.

## What if v-model isn't passed?

If we use `defineModel`, but we won't pass any prop (via either `v-model` or just by normal prop), the `Ref` will be `undefined` by default (unless we provide `default`), but we can still mutate the value and we won't lose any changes. It acts like a wrapper with a local copy:

```typescript
const props = defineProps<{
  modelValue?: Item
}>();
const emit = defineEmits(['update:modelValue'])

const innerValue = ref(props.modelValue);
watch(() => props.modelValue, (newValue) => {
  if (newValue != innerValue.value) {
    innerValue.value = newValue;
  }
});
watch(innerValue, (newValue) => {
  emit('update:modelValue', newValue);
});
```

The actual implementation is of course a bit more complicated, but no need to care about it.

## Reactivity

Here comes the problematic one... While we should **not** mutate props (and their fields), we should emit new values. Therefore, we shouldn't assign anything to `innerValue.value`'s fields, but instead overwrite the `innerValue.value` which will cause the emit:

```typescript
innerValue.value = {
  ...innerValue.value,
  [key]: value
};
```

However, if the prop value was reactive, therefore the `innerValue.value`'s fields will be reactive as well. If you don't care much about good practices, you can easily use `v-model="innerValue.field"` for your input and it will work well.

Mutating fields **won't** call the emit.

You can of course use `toProxy` (explained [here](https://dev.to/razi91/using-v-model-with-custom-setters-4hni)) to keep it clean without having to manually write emits.

## Modifiers! And what the...

Now something "new": we can pass custom modifiers to the `v-model`, just like we do for `input`s: after a dot. It's pretty easily explained in [the official docs](https://vuejs.org/api/sfc-script-setup.html#modifiers-and-transformers), so in short: we can now pass modifiers like `v-model.trim.lower="myValue"` and get them with:

```typescript
const [modelValue, modelModifiers] = defineModel({
  set(value) {
    let toEmit = value;
    if (modelModifiers.trim) {
      toEmit = toEmit.trim();
    }
    if (modelModifiers.lower) {
      toEmit = toEmit.toLowerCase();
    }
    return value
  }
});
```

Basically, any part after a dot lands in `modelModifiers` as a boolean field (either `true` or `undefined`). `modelModifiers` is not reactive, which might be useful.

## Power of static modifiers

All props are reactive, which is nice, but also... annoying, especially if you want to configure the component to act in a way that it shouldn't ever change. It won't, but you can't just ignore the props changes. Okay, you can, but it's not a nice way.

What I mean, is that you can pass a bunch of strings, that you can be sure will never change during the lifetime of the component, therefore there is no need to care about them changing!

For example:

```typescript
<ViewComponent type="horizontal" v-model:context="context">
</ViewComponent>
```

In this template, we know that `type` will never change, but inside the component `type` will be reactive, `eslint` will not allow us to read it losing the reactivity (unless you disable that rule, but don't).

We can now do this:

```typescript
<ViewComponent v-model.horizontal:context="context">
</ViewComponent>
```

And inside:

```typescript
type Options = "horizontal" | "vertical" | "scaled";
const [context, modifiers] = defineModel<ContextType, Options>()

const viewType = modifiers.horizontal ? "horizontal" : "vertical";
const scaled = modifiers.scaled;

if (viewType == "horizontal") {
  // constant sets for horizontal with no need to care 
  // that the `viewType` would ever change
}
```

It's just an empty example presenting the idea and maybe in the future it won't be working that way — maybe modifiers will become reactive? We don't know, I'm just presenting a quick idea, don't take it too seriously.

## What's under the hood?

Okay, but there were no breaking changes here, `defineModel` is "just" another macro that is compiled into an "old" component defining method via `defineComponent`. What happens under the hood and documentation doesn't mention it (though it should!), is that there is added another prop, and it's kinda tricky.

In the simplest example with the default `modelValue`, the SFC compiler produces something like this:

```typescript
const __sfc__ = _defineComponent({
  __name: 'Comp',
  props: {
    "modelValue": { type: String, ...{
  } },
    "modelModifiers": {},
  },
  emits: ["update:modelValue"],
});
```

In case where we also have `defineProps` with one prop called `viewType`:

```typescript
const __sfc__ = _defineComponent({
  __name: 'Comp',
  props: /*#__PURE__*/_mergeModels({
    viewType: { type: String, required: true }
  }, {
    "modelValue": { type: String, ...{
  } },
    "modelModifiers": {},
  }),
});
```

In short, `_mergeModels` just merges those two objects. The first one will contain props from `defineProps`, second the ones produced by `defineModel` (all of them).

We can notice the new prop called `modelModifiers`, that's the one produced by `defineModel`. It's always produced and added, whether we use them or not, probably because otherwise if we try to pass some modifiers without them being defined, they would unnecessarily taint `$attrs`.

So, underneath, modifiers **are** reactive, but it's obfuscated in the code. But what's the problem here?

**Vue adds new hidden props without informing us directly**. If we have a `modelValue`, there is another prop called `modelModifiers`. Also, this name is always a special case.

If we create any other name, there will be defined props called `name` and `nameModifiers`, which also causes a conflict if we have one model called `modelValue` and other... `model` .

```typescript
const modelValue = defineModel<string>();
const model2 = defineModel<string>("model");
const viewType = defineModel<string>("viewType");
```

Will produce this:

```typescript
props: {
  "modelValue": { type: String },
  "modelModifiers": {},
  "model": { type: String },
  "modelModifiers": {},
  "viewType": { type: String },
  "viewTypeModifiers": {},
},
```

I don't think I need to say that this is just wrong... `modelModifiers` appears twice, I hope they will write a name collision detector here, or, since the fact of another prop is being created, they will make the names more unique.

In short: with `defineModel` you need to remember that a prop with the suffix `Modifiers` is virtually added. Also, don't use props that end with `"Modifier"` yourself, or be careful with that.

### From the renderer's viewpoint

If we look at the compiled template, there will be no surprise, that the modifiers are passed just like other attributes/props:

```typescript
_createVNode($setup["Comp"], {
  modelValue: $setup.msg,
  "onUpdate:modelValue": _cache[1] || (_cache[1] = $event => (($setup.msg) = $event)),
  modelModifiers: { trim: true, lower: true },
  class: "item-renderer",
  viewType: $setup.msg,
  "onUpdate:viewType": _cache[2] || (_cache[2] = $event => (($setup.msg) = $event)),
  viewTypeModifiers: { horizontal: true }
}, null, 8 /* PROPS */, ["modelValue", "viewType"])
```

Again, in short: no magic here. An object literal is passed. Anyway, attributes and event listeners also land there. But events always have the `on*` prefix.

## Summary

Among other new features that Vue 3.4 brought, `defineModel` is the most noticeable one (I know that performance is important, but it doesn't change the way you write your code), which will make writing components easier and faster. But it also brings a new feature.

Modifiers are awesome, but you need to remember it adds virtual props.

With that new feature, code will be shorter, and easier to read and you'll have more possibilities.

## EDIT
Okay, it looks like the official docs were supplemented with missing information about additional props being added, when using modifiers, in the expanding `Pre 3.4 Usage
vue` sections.