Skip to content

Template Refs

Consider the following component:

vue
<script setup>
import { ref, onMounted } from "vue";

const divRef = ref(null);

onMounted(() => {
  console.log(divRef.value); // <div>...</div>
});
</script>

<template>
  <div ref="divRef">Hello, Template Refs!</div>
</template>

Compilation Result and Overview

Static ref

vue
<template>
  <div ref="foo">content</div>
</template>

The compilation result is as follows:

js
import { setRef as _setRef, template as _template } from "vue/vapor";
const t0 = _template("<div>content</div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  _setRef(n0, "foo");
  return n0;
}

setRef is a helper function that sets a ref on an element.

Dynamic ref

vue
<template>
  <div :ref="foo">content</div>
</template>

The compilation result is as follows:

js
import {
  renderEffect as _renderEffect,
  setRef as _setRef,
  template as _template,
} from "vue/vapor";
const t0 = _template("<div>content</div>");

function _sfc_render(_ctx) {
  const n0 = t0();
  let r0;
  _renderEffect(() => (r0 = _setRef(n0, _ctx.foo, r0)));
  return n0;
}

For dynamic refs, setRef is called within renderEffect, passing the previous ref value (r0) to unset the old ref.

ref + v-for

vue
<template>
  <div v-for="i in [1, 2, 3]" ref="foo">{{ i }}</div>
</template>

The compilation result is as follows:

js
import {
  setRef as _setRef,
  createFor as _createFor,
  template as _template,
} from "vue/vapor";
const t0 = _template("<div></div>");

function _sfc_render(_ctx) {
  const n0 = _createFor(
    () => [1, 2, 3],
    (_ctx0) => {
      const n2 = t0();
      _setRef(n2, "foo", void 0, true);
      return n2;
    }
  );
  return n0;
}

For refs within v-for, the 4th argument refFor is set to true, managing the ref as an array.

ref + v-if

vue
<template>
  <div v-if="true" ref="foo">content</div>
</template>

The compilation result is as follows:

js
import {
  setRef as _setRef,
  createIf as _createIf,
  template as _template,
} from "vue/vapor";
const t0 = _template("<div>content</div>");

function _sfc_render(_ctx) {
  const n0 = _createIf(
    () => true,
    () => {
      const n2 = t0();
      _setRef(n2, "foo");
      return n2;
    }
  );
  return n0;
}

For refs within v-if, they are set normally but are automatically cleaned up when the block's scope is destroyed.

Reading the Compiler

IR

Let's look at the SET_TEMPLATE_REF IR.

140export interface SetTemplateRefIRNode extends BaseIRNode {
141  type: IRNodeTypes.SET_TEMPLATE_REF
142  element: number
143  value: SimpleExpressionNode
144  refFor: boolean
145  effect: boolean
146}
ts
export interface SetTemplateRefIRNode extends BaseIRNode {
  type: IRNodeTypes.SET_TEMPLATE_REF;
  element: number;
  value: SimpleExpressionNode;
  refFor: boolean;
  effect: boolean;
}
  • element: The ID of the element to set the ref on
  • value: The ref name or expression
  • refFor: Whether inside v-for
  • effect: Whether it's a dynamic ref

Transformer

12export const transformTemplateRef: NodeTransform = (node, context) => {
13  if (node.type !== NodeTypes.ELEMENT) return
14
15  const dir = findProp(node, 'ref', false, true)
16  if (!dir) return
17
18  let value: SimpleExpressionNode
19  if (dir.type === NodeTypes.DIRECTIVE) {
20    value = dir.exp || normalizeBindShorthand(dir.arg!, context)
21  } else {
22    value = dir.value
23      ? createSimpleExpression(dir.value.content, true, dir.value.loc)
24      : EMPTY_EXPRESSION
25  }
26
27  return () => {
28    const id = context.reference()
29    const effect = !isConstantExpression(value)
30    effect &&
31      context.registerOperation({
32        type: IRNodeTypes.DECLARE_OLD_REF,
33        id,
34      })
35    context.registerEffect([value], {
36      type: IRNodeTypes.SET_TEMPLATE_REF,
37      element: id,
38      value,
39      refFor: !!context.inVFor,
40      effect,
41    })
42  }
43}
ts
export const transformTemplateRef: NodeTransform = (node, context) => {
  if (node.type !== NodeTypes.ELEMENT) return;

  const dir = findProp(node, "ref", false, true);
  if (!dir) return;

  let value: SimpleExpressionNode;
  if (dir.type === NodeTypes.DIRECTIVE) {
    // For :ref="foo"
    value = dir.exp || normalizeBindShorthand(dir.arg!, context);
  } else {
    // For ref="foo"
    value = dir.value
      ? createSimpleExpression(dir.value.content, true, dir.value.loc)
      : EMPTY_EXPRESSION;
  }

  return () => {
    const id = context.reference();
    const effect = !isConstantExpression(value);

    // For dynamic refs, declare a variable to track the old ref
    effect &&
      context.registerOperation({
        type: IRNodeTypes.DECLARE_OLD_REF,
        id,
      });

    context.registerEffect([value], {
      type: IRNodeTypes.SET_TEMPLATE_REF,
      element: id,
      value,
      refFor: !!context.inVFor,
      effect,
    });
  };
};

Key points:

  1. Detect ref attribute or :ref directive
  2. Determine if static or dynamic (isConstantExpression)
  3. For dynamic, declare a variable to track the old ref with DECLARE_OLD_REF
  4. Register the ref setting with SET_TEMPLATE_REF

Reading the Runtime

setRef is implemented in dom/templateRef.ts in runtime-vapor.

30export function setRef(
31  el: RefEl,
32  ref: NodeRef,
33  oldRef?: NodeRef,
34  refFor = false,
35): NodeRef | undefined {
36  if (!currentInstance) return
37  const { setupState, isUnmounted } = currentInstance
38
39  if (isUnmounted) {
40    return
41  }
42
43  const refValue = isVaporComponent(el) ? el.exposed || el : el
44
45  const refs =
46    currentInstance.refs === EMPTY_OBJ
47      ? (currentInstance.refs = {})
48      : currentInstance.refs
49
50  // dynamic ref changed. unset old ref
51  if (oldRef != null && oldRef !== ref) {
52    if (isString(oldRef)) {
53      refs[oldRef] = null
54      if (hasOwn(setupState, oldRef)) {
55        setupState[oldRef] = null
56      }
57    } else if (isRef(oldRef)) {
58      oldRef.value = null
59    }
60  }
61
62  if (isFunction(ref)) {
63    const invokeRefSetter = (value?: Element | Record<string, any>) => {
64      callWithErrorHandling(
65        ref,
66        currentInstance,
67        VaporErrorCodes.FUNCTION_REF,
68        [value, refs],
69      )
70    }
71
72    invokeRefSetter(refValue)
73    onScopeDispose(() => invokeRefSetter())
74  } else {
75    const _isString = isString(ref)
76    const _isRef = isRef(ref)
77    let existing: unknown
78
79    if (_isString || _isRef) {
80      const doSet: SchedulerJob = () => {
81        if (refFor) {
82          existing = _isString
83            ? hasOwn(setupState, ref)
84              ? setupState[ref]
85              : refs[ref]
86            : ref.value
87
88          if (!isArray(existing)) {
89            existing = [refValue]
90            if (_isString) {
91              refs[ref] = existing
92              if (hasOwn(setupState, ref)) {
93                setupState[ref] = refs[ref]
94                // if setupState[ref] is a reactivity ref,
95                // the existing will also become reactivity too
96                // need to get the Proxy object by resetting
97                existing = setupState[ref]
98              }
99            } else {
100              ref.value = existing
101            }
102          } else if (!existing.includes(refValue)) {
103            existing.push(refValue)
104          }
105        } else if (_isString) {
106          refs[ref] = refValue
107          if (hasOwn(setupState, ref)) {
108            setupState[ref] = refValue
109          }
110        } else if (_isRef) {
111          ref.value = refValue
112        } else if (__DEV__) {
113          warn('Invalid template ref type:', ref, `(${typeof ref})`)
114        }
115      }
116      doSet.id = -1
117      queuePostFlushCb(doSet)
118
119      onScopeDispose(() => {
120        queuePostFlushCb(() => {
121          if (isArray(existing)) {
122            remove(existing, refValue)
123          } else if (_isString) {
124            refs[ref] = null
125            if (hasOwn(setupState, ref)) {
126              setupState[ref] = null
127            }
128          } else if (_isRef) {
129            ref.value = null
130          }
131        })
132      })
133    } else if (__DEV__) {
134      warn('Invalid template ref type:', ref, `(${typeof ref})`)
135    }
136  }
137  return ref
138}
ts
export function setRef(
  el: RefEl,
  ref: NodeRef,
  oldRef?: NodeRef,
  refFor = false
): NodeRef | undefined {
  if (!currentInstance) return;
  const { setupState, isUnmounted } = currentInstance;

  if (isUnmounted) {
    return;
  }

  const refValue = isVaporComponent(el) ? el.exposed || el : el;
  const refs =
    currentInstance.refs === EMPTY_OBJ
      ? (currentInstance.refs = {})
      : currentInstance.refs;

  // If dynamic ref changed, unset the old ref
  if (oldRef != null && oldRef !== ref) {
    if (isString(oldRef)) {
      refs[oldRef] = null;
      if (hasOwn(setupState, oldRef)) {
        setupState[oldRef] = null;
      }
    } else if (isRef(oldRef)) {
      oldRef.value = null;
    }
  }

  // ...
}

Types of Refs

setRef supports 3 types of refs:

  1. String ref: ref="foo" - Set to setupState.foo or refs.foo
  2. Ref object: :ref="myRef" - Set to myRef.value
  3. Function ref: :ref="(el) => ..." - Calls the function

Array refs in v-for

When refFor = true, refs are managed as an array:

ts
if (refFor) {
  existing = _isString
    ? hasOwn(setupState, ref)
      ? setupState[ref]
      : refs[ref]
    : ref.value;

  if (!isArray(existing)) {
    existing = [refValue];
    // ...
  } else if (!existing.includes(refValue)) {
    existing.push(refValue);
  }
}

Cleanup

Using onScopeDispose, refs are cleaned up when the scope is destroyed:

ts
onScopeDispose(() => {
  queuePostFlushCb(() => {
    if (isArray(existing)) {
      remove(existing, refValue);
    } else if (_isString) {
      refs[ref] = null;
      if (hasOwn(setupState, ref)) {
        setupState[ref] = null;
      }
    } else if (_isRef) {
      ref.value = null;
    }
  });
});

This ensures that refs are automatically cleaned up when blocks are destroyed in v-if or v-for.

queuePostFlushCb

Ref setting is registered to the Post Flush Queue using queuePostFlushCb:

ts
const doSet: SchedulerJob = () => {
  // ref setting logic
};
doSet.id = -1;
queuePostFlushCb(doSet);

id = -1 ensures execution before other jobs.
This means refs are set after DOM updates but before other effects.


Template Refs in Vapor Mode provide the same behavior as the Options API.
It supports all use cases including dynamic refs, array refs in v-for, and function refs.
By utilizing onScopeDispose, proper cleanup is also guaranteed.