Documenting scenarios encountered when using DOM elements in immediately executed watch with Vue3 setup syntax.

Requirement: After DOM element exists, perform DOM operations whenever certain data changes. For example, when using third-party charts, we need to update the chart based on data changes after the element exists.

<div ref="el"></div>
const el = ref();
const data = computed(() => "your data");
watch(
  () => [el.value, data.value],
  () => {
    if (!el.value) return;
    // DOM operations
    console.log(document.body.clientHeight);
  },
  { immediate: true }
);

Some might place watch inside onMounted to ensure DOM is loaded. While this works, it's risky. Watches not executed synchronously in setup or after any await won't be properly associated with the component by Vue, potentially causing memory leaks when the component is destroyed (though you can manually stop such watches - see official docs).

Experiment

<div ref="el"></div>
const el = ref();
const data = computed(() => "your data");
watch(
  () => [el.value, data.value],
  async () => {
    if (!el.value) return;
    // Get body height
    console.log(document.body.clientHeight);
    await $nextTick();
    // Get body height again
    console.log(document.body.clientHeight);
  },
  { immediate: true }
);
Body Height ExperimentFirstAfter $nextTick
VueHeight is 0Normal height
NuxtError: document undefinedNormal height
Nuxt 不开启 ssrHeight is 0Height is 0

Nuxt Solution

I designed this experiment because I encountered issues with Echarts which requires non-zero element dimensions.

From the results, Vue and SSR-enabled Nuxt behave as expected, while non-SSR Nuxt shows unexpected behavior. The solution is simple: regardless of SSR status, wrap DOM elements (like charts, sliders, video players) needing operations with Nuxt's built-in client-only component. This ensures proper dimensions when the element becomes available.

<client-only>
  <div ref="el"></div>
</client-only>

ref in v-for

<div v-for="...">
  <div ref="el"></div>
</div>

In this case, el.value will be an array - be aware of this behavior.