ref → función de composición reactiva
Herramienta: ref
Framework: Vue
Persona Encargada: Ariel GonzAgüer
Creado: 16-9-2025
Última actualización: 16-9-2025
Este artículo cubre todo lo que necesita saber sobre ref
en Vue 3: desde lo básico hasta casos avanzados.
¿Qué es ref
?
ref
es la función principal para crear estado reactivo en Vue 3. Es el equivalente más cercano a useState
de React, pero con una API diferente. Devuelve un objeto con una propiedad .value
que contiene el valor y activa la reactividad cuando cambia.
// Dispara reactividad ```
</div>
<Ref client:load />
## 1. Ref con Primitivas
### Números, strings y booleanos
<div className='bloqueCodigo'>
```vue
<script setup>
import { ref } from 'vue';
const count = ref(0);
const message = ref('Hola Vue');
const isVisible = ref(true);
const increment = () => count.value++;
const toggle = () => isVisible.value = !isVisible.value;
</script>
<template>
<div>
<p>Contador: {{ count }}</p>
<button @click="increment">+1</button>
<p v-if="isVisible">{{ message }}</p>
<button @click="toggle">Alternar visibilidad</button>
</div>
</template>
Importante: En el template, Vue desempaqueta automáticamente .value
, pero en JavaScript siempre necesita usarlo.
2. Ref con Objetos y Arrays
Objetos simples
<script setup>
import { ref } from 'vue';
const user = ref({
name: 'Ana',
age: 25,
email: 'ana@example.com'
});
const updateName = () => {
user.value.name = 'Ana García'; // Vue detecta este cambio
};
const updateUser = () => {
// Reemplazar todo el objeto también funciona
user.value = {
name: 'Carlos',
age: 30,
email: 'carlos@example.com'
};
};
</script>
<template>
<div>
<h3>{{ user.name }} ({{ user.age }} años)</h3>
<p>{{ user.email }}</p>
<button @click="updateName">Cambiar nombre</button>
<button @click="updateUser">Cambiar usuario</button>
</div>
</template>
Arrays y mutaciones
<script setup>
import { ref } from 'vue';
const items = ref(['Manzana', 'Banana', 'Naranja']);
const newItem = ref('');
const addItem = () => {
if (newItem.value.trim()) {
items.value.push(newItem.value); // Mutación detectada
newItem.value = '';
}
};
const removeItem = (index) => {
items.value.splice(index, 1); // Mutación detectada
};
const sortItems = () => {
items.value.sort(); // Mutación detectada
};
</script>
<template>
<div>
<input v-model="newItem" placeholder="Nuevo elemento" />
<button @click="addItem">Añadir</button>
<button @click="sortItems">Ordenar</button>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item }}
<button @click="removeItem(index)">×</button>
</li>
</ul>
</div>
</template>
3. Ref vs Reactive
Cuándo usar ref
- Valores primitivos (string, number, boolean)
- Cuando necesitas reemplazar toda la referencia
- Para consistencia en APIs de composables
- Variables que pueden ser
null
oundefined
Cuándo usar reactive
- Objetos complejos que no cambiarán su referencia
- Cuando quiere sintaxis más limpia (sin
.value
) - Estados con múltiples propiedades relacionadas
<script setup>
import { ref, reactive } from 'vue';
// Ref: ideal para primitivas o cuando la referencia puede cambiar
const count = ref(0);
const user = ref(null); // Puede ser null inicialmente
// Reactive: ideal para objetos estables
const state = reactive({
loading: false,
error: null,
data: []
});
// Comparación de uso
const updateWithRef = () => {
user.value = { name: 'Juan', age: 28 }; // Reemplaza toda la referencia
};
const updateWithReactive = () => {
state.loading = true; // Sin .value
state.data = ['item1', 'item2'];
};
</script>
4. Computed Properties
Los computed
son refs de solo lectura que se recalculan automáticamente cuando sus dependencias cambian:
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('Ana');
const lastName = ref('García');
// Computed de solo lectura
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// Computed con getter y setter
const fullNameEditable = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const parts = value.split(' ');
firstName.value = parts[0] || '';
lastName.value = parts[1] || '';
}
});
const items = ref([1, 2, 3, 4, 5]);
const evenItems = computed(() => items.value.filter(n => n % 2 === 0));
const itemCount = computed(() => items.value.length);
</script>
<template>
<div>
<input v-model="firstName" placeholder="Nombre" />
<input v-model="lastName" placeholder="Apellido" />
<p>Nombre completo: {{ fullName }}</p>
<input v-model="fullNameEditable" placeholder="Nombre completo editable" />
<p>Total de elementos: {{ itemCount }}</p>
<p>Números pares: {{ evenItems.join(', ') }}</p>
</div>
</template>
5. Watchers
Observa cambios en refs y ejecuta efectos secundarios:
<script setup>
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const user = ref({ name: 'Ana', age: 25 });
const searchTerm = ref('');
// Watch básico
watch(count, (newValue, oldValue) => {
console.log(`Count cambió de ${oldValue} a ${newValue}`);
});
// Watch múltiples fuentes
watch([count, searchTerm], ([newCount, newSearch], [oldCount, oldSearch]) => {
console.log('Múltiples valores cambiaron');
});
// Watch profundo para objetos
watch(user, (newUser, oldUser) => {
console.log('Usuario cambió:', newUser);
}, { deep: true });
// Watch inmediato (se ejecuta al montar)
watch(searchTerm, (term) => {
// Simular búsqueda API
console.log(`Buscando: ${term}`);
}, { immediate: true });
// WatchEffect (detecta dependencias automáticamente)
watchEffect(() => {
// Se ejecuta inmediatamente y cuando count o searchTerm cambien
console.log(`Efecto: count=${count.value}, search=${searchTerm.value}`);
});
// Watch con cleanup
watchEffect((onInvalidate) => {
const timeoutId = setTimeout(() => {
console.log('Timeout ejecutado');
}, 1000);
onInvalidate(() => {
clearTimeout(timeoutId); // Limpia el timeout si el efecto se reinicia
});
});
</script>
6. Template Refs (Referencias DOM)
Use ref
para acceder directamente a elementos DOM:
<script setup>
import { ref, onMounted, nextTick } from 'vue';
// Ref para elemento único
const inputRef = ref(null);
const divRef = ref(null);
// Ref para lista de elementos
const itemRefs = ref([]);
const focusInput = () => {
inputRef.value?.focus();
};
const scrollToBottom = () => {
divRef.value?.scrollTo(0, divRef.value.scrollHeight);
};
const getItemDimensions = () => {
itemRefs.value.forEach((el, index) => {
if (el) {
console.log(`Item ${index}:`, el.getBoundingClientRect());
}
});
};
onMounted(() => {
// El DOM está disponible
console.log('Input element:', inputRef.value);
});
// Función para asignar refs en v-for
const setItemRef = (el) => {
if (el) {
itemRefs.value.push(el);
}
};
</script>
<template>
<div>
<input ref="inputRef" placeholder="Input con ref" />
<button @click="focusInput">Enfocar input</button>
<div ref="divRef" style="height: 200px; overflow-y: auto;">
<div
v-for="n in 20"
:key="n"
:ref="setItemRef"
style="height: 50px; border: 1px solid #ccc; margin: 5px;"
>
Item {{ n }}
</div>
</div>
<button @click="scrollToBottom">Scroll al final</button>
<button @click="getItemDimensions">Ver dimensiones</button>
</div>
</template>
7. Composables con Refs
Cree lógica reutilizable encapsulando refs:
// composables/useCounter.js
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => (count.value = initialValue);
const isEven = computed(() => count.value % 2 === 0);
const isPositive = computed(() => count.value > 0);
return {
count: readonly(count), // Opcional: hacer read-only
increment,
decrement,
reset,
isEven,
isPositive,
};
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key);
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
watch(
value,
newValue => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true }
);
return value;
}
// composables/useFetch.js
import { ref } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const execute = async () => {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
return { data, error, loading, execute };
}
Usando los composables:
<script setup>
import { useCounter } from './composables/useCounter.js';
import { useLocalStorage } from './composables/useLocalStorage.js';
import { useFetch } from './composables/useFetch.js';
// Counter reutilizable
const { count, increment, decrement, isEven } = useCounter(10);
// Persistencia en localStorage
const preferences = useLocalStorage('user-preferences', {
theme: 'light',
language: 'es'
});
// Fetch de datos
const { data: users, loading, execute: fetchUsers } = useFetch('/api/users');
// Ejecutar fetch al montar
onMounted(fetchUsers);
</script>
<template>
<div>
<h3>Counter: {{ count }} ({{ isEven ? 'Par' : 'Impar' }})</h3>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<h3>Preferencias:</h3>
<select v-model="preferences.theme">
<option value="light">Claro</option>
<option value="dark">Oscuro</option>
</select>
<h3>Usuarios:</h3>
<div v-if="loading">Cargando...</div>
<ul v-else-if="users">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</div>
</template>
8. Casos Avanzados
Refs anidados y unwrapping
<script setup>
import { ref, isRef, unref, toRef, toRefs } from 'vue';
const count = ref(0);
const nested = ref({ count }); // Ref dentro de ref
// Vue automáticamente "unwrapea" refs anidados en objetos reactivos
console.log(nested.value.count); // Es un número, no un ref
// Utilidades para trabajar con refs
const checkIfRef = () => {
console.log('count es ref:', isRef(count)); // true
console.log('valor de count:', unref(count)); // 0 (siempre devuelve el valor)
};
// toRef: crea ref desde propiedad de objeto reactivo
const user = reactive({ name: 'Ana', age: 25 });
const nameRef = toRef(user, 'name'); // ref que apunta a user.name
// toRefs: convierte todas las propiedades a refs
const { name, age } = toRefs(user); // Ahora son refs individuales
</script>
Custom ref con customRef
<script setup>
import { customRef } from 'vue';
// Ref con debounce personalizado
function useDebouncedRef(value, delay = 300) {
let timeoutId;
return customRef((track, trigger) => ({
get() {
track(); // Registra la dependencia
return value;
},
set(newValue) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
value = newValue;
trigger(); // Dispara la reactividad
}, delay);
}
}));
}
const searchQuery = useDebouncedRef('');
// El efecto solo se ejecuta después del delay
watchEffect(() => {
console.log('Búsqueda:', searchQuery.value);
});
</script>
Performance y optimizaciones
<script setup>
import { ref, shallowRef, triggerRef, markRaw } from 'vue';
// shallowRef: solo reactivo en el primer nivel
const largeObject = shallowRef({
data: new Array(10000).fill(0).map((\_, i) => ({ id: i, value: i \* 2 }))
});
// Para actualizar después de mutar el objeto interno
const updateLargeObject = () => {
largeObject.value.data[0].value = 999;
triggerRef(largeObject); // Fuerza actualización manual
};
// markRaw: marca objeto como no reactivo
const nonReactiveObject = markRaw({
expensiveData: new Map(),
heavyComputation: () => {/* ... */}
});
</script>
9. Patrones y Mejores Prácticas
1. Convenciones de nomenclatura
// ✅ Buenos nombres
const isLoading = ref(false);
const userList = ref([]);
const selectedUser = ref(null);
// ❌ Evitar nombres confusos
const data = ref({}); // ¿Qué tipo de data?
const flag = ref(true); // ¿Qué flag?
2. Inicialización defensiva
// ✅ Valores por defecto claros
const users = ref([]);
const currentUser = ref(null);
const config = ref({
theme: 'light',
notifications: true
});
// ✅ Validación en computed
const isValidUser = computed(() => {
return currentUser.value &&
currentUser.value.email &&
currentUser.value.email.includes('@');
});
3. Cleanup y memoria
<script setup>
import { ref, watchEffect } from 'vue';
const data = ref([]);
let cleanup;
watchEffect(() => {
cleanup = setInterval(() => {
// Alguna lógica
}, 1000);
});
// Cleanup automático en unmount
onUnmounted(() => {
if (cleanup) clearInterval(cleanup);
});
</script>
10. Debugging Refs
DevTools
Vue DevTools muestra el valor de los refs y permite editarlos en tiempo real.
Console debugging
import { ref } from 'vue';
const count = ref(0);
// Ver el objeto ref completo
console.log(count); // RefImpl { \_value: 0, ... }
// Ver solo el valor
console.log(count.value); // 0
// Agregar debug a computed
const doubleCount = computed(() => {
const result = count.value \* 2;
console.log(`doubleCount calculado: ${result}`);
return result;
});
Resumen
ref
es la piedra angular de la reactividad en Vue 3:
- Primitivas:
ref(valor)
para números, strings, booleanos - Objetos/Arrays: Totalmente reactivos, detecta mutaciones profundas
- Templates: Auto-unwrapping, no necesita
.value
- JavaScript: Siempre use
.value
para leer/escribir - Computed: Refs derivados que se recalculan automáticamente
- Watch: Observa cambios y ejecuta efectos secundarios
- DOM: Template refs para acceso directo a elementos
- Composables: Encapsula lógica reutilizable con refs
- Performance:
shallowRef
,triggerRef
,markRaw
para casos especiales
La clave está en entender cuándo usar ref
vs reactive
, y aprovechar el ecosistema de composables para crear código mantenible y reutilizable.