vue.jsvuejs3

prevent watched elements from calling updates on each other


How can I prevent watched elements from calling updates on each other in Vuejs?

const width = ref(0);
const height = ref(0);

watch(width, async (newItem, oldItem) => {
    console.log(`width: ${oldItem}->${newItem}`);
    height.value = newItem / 2;
});

watch(height, async (newItem, oldItem) => {
    console.log(`height: ${oldItem}->${newItem}`);
    width.value = newItem * 2;
});

as a result, I can see output like this in the console

width: 1->2
height: 0.5->1
width: 2->2

Full vuejs code:

<script setup>
import { ref } from 'vue'
import { watch } from 'vue'

const width = ref(0);
const height = ref(0);

watch(width, async (newItem, oldItem) => {
    console.log(`width: ${oldItem}->${newItem}`);
    height.value = newItem / 2;
});

watch(height, async (newItem, oldItem) => {
    console.log(`height: ${oldItem}->${newItem}`);
    width.value = newItem * 2;
});
</script>

<template>
    <div class="item">
        <div class="details">
            <h3>
                Width {{ width }}
            </h3>
            <input v-model="width" placeholder="0" />
        </div>
    </div>

    <div class="item">
        <div class="details">
            <h3>
                Height {{ height }}
            </h3>
            <input v-model="height" placeholder="0" />
        </div>
    </div>
</template>


Solution

  • If you output the values with JSON.stringify you'll notice that the type is changed from string to number due calculations: Playground

    width: 0->"2"
    height: 0->1
    width: "2"->2
    

    Just use v-model.number="width" to operate on numbers only: Playground

    To avoid entering non-numeric values I would suggest to use keydown event:

    const preventAlpha = e => {
        if(/[^\d.]/.test(e.key) && e.key.length === 1){
            e.preventDefault();
        } else 
        if(e.key === '.' && e.target.value.includes('.')){
            e.preventDefault();
        } else 
        if(e.target.value === '' && e.key === '.'){
            e.target.value = '0';
        }
    }
    const vNumber = {
        mounted: el => el.addEventListener('keydown', preventAlpha),
        unmounted: el => el.removeEventListener('keydown', preventAlpha)
    }
    

    Playground

    UPDATE Answer the comment: there's usually no need to prevent the second watch since it's settled after and no further watches are invoked. Otherwise I don't know a way to change a ref silently, but you could make manual checks:

    let changing = false;
    
    watch(width, async (newItem, oldItem) => {
        if(changing) return;
        console.log(`width: ${JSON.stringify(oldItem)}->${JSON.stringify(newItem)}`);
        changing = true;
        height.value = newItem / 2;
        await 1;
        changing = false;
    });
    
    watch(height, async (newItem, oldItem) => {
        if(changing) return;
        console.log(`height: ${JSON.stringify(oldItem)}->${JSON.stringify(newItem)}`);
        changing = true;
        width.value = newItem * 2;
        await 1;
        changing = false;
    });
    

    Playground