¡Código - Código!

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

Cuándo usar reactive

<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:

La clave está en entender cuándo usar ref vs reactive, y aprovechar el ecosistema de composables para crear código mantenible y reutilizable.