VueNuxt.dev
Vue Insights

Computed Model

Two-way data binding for computed properties

Intro

Two-way data binding is one of Vue's core patterns. This is best reflected by the v-model directive. It allows us to pass data down to child components and listen to events from the child in the parent. This process abstracts two wirings, the data prop, and the event listener, with a single directive.

I curious to see if we can we achieve the same two-way data binding through a computed property?


Example

const dev = ref('vue')
const ex = ref('nuxt')

const devex = computed(() => `${toValue(dev)}${toValue(ex)}.com`)

console.log(toValue(devex))
// ^ this would output `vuenuxt.com` as expected

Any change on dev or ex would trigger a recomputation of devex. This is what computer properties are known for; a change in any of its dependencies will trigger the computed logic in the callback to re-run and return an updated value. That's one-way data binding deps -> computed logic.


Problem

But how can we change the value of the dependencies through the computed property itself, computed -> deps? If we try to assign a new value to devex, we will get a Vue Warning because computed properties are read-only by default. This makes sense because we have no way of knowing how to match the input to the schema of the dependencies.

const dev = ref('vue')
const ex = ref('nuxt')

const devex = computed(() => `${toValue(dev)}${toValue(ex)}.com`)

devex.value = 'vue some'
// ^ [Vue warn] Write operation failed: computed value is readonly

Insight

We are used to passing an anonymous function as an argument into the computed function. Still, we can also pass an object with getters and setters to extend its functionality. The getter logic shapes the output. The setter logic allows us to do whatever we want with the input. This change not only turns off the read-only limitation but opens the path for two-way binding. deps <-> computed

const dev = ref('vue')
const ex = ref('nuxt')

type Devex = `${string} ${string}`
const devex = computed({
  get() {
    return `${toValue(dev)}${toValue(ex)}.com`
  },
  set(newValue: Devex) {
    [dev.value, ex.value] = newValue.split(' ')
  }
})

setTimeout(() => {
  devex.value = 'vue some'
}, 3000)

Notice: How we define a more constrained string type for our input in the ArrayExample.vue to ensure the newValue.split(' ') in the setter will work as expected.


Mechanism

After the timer runs, we set the value of devex(red) with vue some(green), which triggers the setter(purple) path. The setter logic changes the value of the refs dev(blue) and ex(blue). This update triggers a re-run of the getter(purple) path for devex(red) that outputs a newly computed value of vuesome.com(orange).

That's computed two-way data binding. How vuesome is that!

Paths:

  • setter: dotted lines
  • getter: solid lines

Applications

V-Model

This application allows us to use v-model through a computed property in the usual parent-child component relationship. We emit events in the child component because we do not want to mutate props. This slightly increases the wiring in the child component but makes it easier to reuse this component in other parts through the standard v-model pattern.

We change the value of the model using three timeouts:

  1. sets a new value for the computed property in the parent component
  2. emits a new value from the child component, which is picked up by the computed property in the parent
  3. mutates a dependant ref directly in the parent component
type DevexInput = `${string} ${string}`;

export const useComputedModel = () => {
  const dev = ref('vue');
  const ex = ref('nuxt');

  return {
    dev: readonly(dev),
    ex,
    computedModel: computed({
      get() {
        return `${toValue(dev)}${toValue(ex)}.com`;
      },
      set(newValue: DevexInput) {
        [dev.value, ex.value] = newValue.split(' ');
      },
    }),
  };
};
Open Playground

Provider

This application is a more powerful but less standard alternative to the v-model approach presented above. Instead of binding through a v-model directly on child components, we provide the computed model from the parent to all its nested components. This approach allows two-way data binding in all child components without having to prop-drill or wire event listeners along the way.

For simplicity, I use three levels for this example:

-| Parent.vue
---| Child.vue
-----| GrandChild.vue

We change the value of the model using three timeouts:

  1. sets a new value for the computed property in the child component.
  2. sets a new value for the computed property in the grandchild component.
  3. mutates a dependant ref directly in the parent component.
type DevexInput = `${string} ${string}`

export const useComputedModel = () => {

  const dev = ref('vue')
  const ex = ref('nuxt')

  const computedModel = computed({
    get() {
      return `${toValue(dev)}${toValue(ex)}.com`
    },
    set(newValue) {
      [dev.value, ex.value] = newValue.split(' ')
    }
  })

  return {
    dev,
    ex,
    computedModel,
  }
}
Open Playground

Copyright © 2025