Patrones de Componentes y Composables de Vue
Jozo
Jozo
2025/03/18
12 min read

Patrones de Componentes y Composables de Vue: Una Guía de Inicio para Startups

Esta guía proporciona una visión práctica de patrones de diseño de componentes y composables de Vue, con consejos y ejemplos, adaptados para desarrolladores en tu startup. Aprovecha información de varias fuentes para ayudarte a escribir aplicaciones Vue más limpias, mantenibles y escalables.

Patrones de Diseño de Componentes

1. Patrón de Componentes

Extraer componentes reutilizables de componentes existentes simplifica el código y mejora la reutilización^1. Esto promueve el Principio de Responsabilidad Única, haciendo tu base de código más modular y mantenible.

Consejo: Identifica y extrae componentes ocultos dentro de tu código existente. Busca elementos de interfaz de usuario repetidos o lógica que pueda ser encapsulada.

Ejemplo:

<!-- Antes: Formulario Complejo -->
<template>
  <div>
    <label for="name">Nombre:</label>
    <input type="text" id="name" v-model="name">
    <label for="email">Email:</label>
    <input type="email" id="email" v-model="email">
    <button @click="submitForm">Enviar</button>
  </div>
</template>
<!-- Después: Usando Componentes Reutilizables -->
<template>
  <div>
    <InputField label="Nombre" v-model="name" type="text" />
    <InputField label="Email" v-model="email" type="email" />
    <SubmitButton @click="submitForm" />
  </div>
</template>

2. Componentes Limpios

Apunta a componentes que no solo funcionen sino que funcionen bien, considerando legibilidad del código, mantenibilidad y capacidad de prueba^1. Los componentes limpios son fáciles de entender, modificar y depurar.

Consejo: Escribe componentes que sean fáciles de entender y mantener. Usa convenciones de nombres claras, formato consistente y responsabilidades bien definidas.

Ejemplo:

<!-- Malo: Componente con preocupaciones mixtas -->
<template>
  <div>
    <button @click="handleClick">{{ buttonText }}</button>
    <div v-if="showDetails">{{ details }}</div>
  </div>
</template>
<script setup>
import { ref } from 'vue';

const buttonText = ref('Mostrar Detalles');
const showDetails = ref(false);
const details = ref('');

async function handleClick() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>
<!-- Bueno: Componente con responsabilidad enfocada -->
<template>
  <div>
    <ShowDetailsButton @click="toggleDetails" :text="buttonText" />
    <DetailsDisplay v-if="showDetails" :details="details" />
  </div>
</template>
<script setup>
import { ref } from 'vue';
import ShowDetailsButton from './ShowDetailsButton.vue';
import DetailsDisplay from './DetailsDisplay.vue';

const showDetails = ref(false);
const details = ref('');
const buttonText = ref('Mostrar Detalles');

async function toggleDetails() {
  showDetails.value = !showDetails.value;
  if (showDetails.value) {
    details.value = await fetchData();
  }
}
</script>

3. Múltiples Componentes en Un Archivo

Para componentes pequeños y autónomos, considera mantenerlos en el mismo archivo^2. Esto puede reducir la cantidad de archivos en tu proyecto y mejorar la velocidad de desarrollo, especialmente para componentes que están fuertemente acoplados.

Consejo: Evita crear archivos innecesarios para componentes simples. Usa este enfoque para componentes que solo se usan en un lugar.

Ejemplo:

<template>
  <div>
    <MyButton @click="handleClick">Haz Clic Aquí</MyButton>
  </div>
</template>

<script setup>
import MyButton from './MyButton.vue';

function handleClick() {
  alert('¡Botón clickeado!');
}
</script>
<template>
  <button @click="$emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
defineEmits(['click']);
</script>

4. Patrón de Props Controlados

Este patrón te permite anular el estado interno de un componente desde el padre^4. Esto es útil cuando necesitas forzar el estado de un componente desde afuera, como controlar la visibilidad de un modal o la selección en un dropdown.

Consejo: Usa este patrón cuando necesites forzar el estado de un componente desde afuera. Pasa props al componente para controlar su estado interno.

Ejemplo:

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="closeModal">Cerrar</button>
    </div>
  </div>
</template>
<script setup>
defineProps({
  isOpen: {
    type: Boolean,
    default: false
  }
});

const emit = defineEmits(['close']);

function closeModal() {
  emit('close');
}
</script>
<!-- Componente Padre -->
<template>
  <div>
    <button @click="showModal = true">Abrir Modal</button>
    <Modal :isOpen="showModal" @close="showModal = false">
      <p>Contenido del Modal</p>
    </Modal>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';

const showModal = ref(false);
</script>

5. Metadatos de Componentes

Añade metadatos a componentes para proporcionar información adicional a otros componentes ^2. Esto se puede usar para configuración de componentes, para pasar información adicional, o para facilitar la comunicación entre componentes.

Consejo: Usa metadatos para configuración de componentes o para pasar información adicional. Esto puede ser útil para herramientas o para proporcionar contexto a otros componentes.

Ejemplo:

<!-- Componente A -->
<template>
  <div>Componente A</div>
</template>
<script>
export default {
  meta: {
    componentType: 'display'
  }
}
</script>
<!-- Componente B -->
<template>
  <div>Componente B</div>
</template>
<script>
export default {
  meta: {
    componentType: 'formField'
  }
}
</script>

Patrones de Diseño de Composables

1. Patrón de Objeto de Opciones

Usa un objeto para pasar parámetros en composables^1. Esto permite flexibilidad y escalabilidad. Es el método preferido para pasar numerosas opciones a un composable.

Consejo: Este patrón se usa en VueUse y es altamente recomendado cuando necesitas configurar el comportamiento de un composable.

Ejemplo:

// useFetch.js
import { ref, onMounted } from 'vue';

export function useFetch(url, options = {}) {
  const data = ref(null);
  const loading = ref(false);
  const error = ref(null);

  const { method = 'GET', headers = {}, body = null } = options;

  async function fetchData() {
    loading.value = true;
    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : null
      });
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      data.value = await response.json();
    } catch (e) {
      error.value = e;
    } finally {
      loading.value = false;
    }
  }

  onMounted(() => {
    fetchData();
  });

  return { data, loading, error, fetchData };
}

// En un componente:
import { useFetch } from './useFetch';
export default {
  setup() {
    const { data, loading, error } = useFetch('/api/data', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { key: 'value' }
    });
    return { data, loading, error };
  }
}

2. Composables en Línea

Crea composables directamente dentro del archivo del componente para evitar crear nuevos archivos^1. Esto es particularmente útil para composables muy específicos de un componente y no destinados para reutilización en otro lado. Usa composables en línea para lógica pequeña y específica del componente. Esto mantiene código relacionado junto y puede simplificar el desarrollo.

Consejo: Usa composables en línea para lógica pequeña y específica del componente que no necesita reutilizarse en otro lugar. Este enfoque mantiene código relacionado junto y simplifica tu estructura de componentes.

Ejemplo:

Digamos que tienes esto en tu componente:

<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const useFormattedDate = () => {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}

const { formattedDate } = useFormattedDate()
</script>

Está bien por ahora, pero si necesitas este formato de fecha en otro lugar, estás atrapado. Aquí está la versión refactorizada:

// src/composables/useFormattedDate.ts
import { ref, computed } from 'vue'

export function useFormattedDate() {
  const rawDate = ref(new Date())
  const formattedDate = computed(() => {
    const options = { year: 'numeric', month: 'long', day: 'numeric' }
    return rawDate.value.toLocaleDateString(undefined, options)
  })
  return { formattedDate }
}
<!-- En tu componente -->
<template>
  <div>{{ formattedDate }}</div>
</template>

<script setup>
import { useFormattedDate } from './composables/useFormattedDate'

const { formattedDate } = useFormattedDate()
</script>

Ahora, puedes reutilizar useFormattedDate en cualquier componente. Problema resuelto.

3. Codificación de Mejores Composables

Extrae pequeños fragmentos de lógica en funciones que puedas reutilizar repetidamente^1. Esto promueve reutilización de código, reduce duplicación, y hace tu código más mantenible.

Consejo: Usa composables para organizar y reutilizar lógica empresarial. Piensa en composables como bloques de construcción reutilizables para tu aplicación.

Ejemplo:

// useLocalStorage.ts
import { ref, watch } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const storedValue = localStorage.getItem(key);
  const value = ref<T>(storedValue !== null ? JSON.parse(storedValue) : defaultValue);

  watch(
    value,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return value;
}

// Uso en un componente:
<script setup>
import { useLocalStorage } from './useLocalStorage';

const theme = useLocalStorage('theme', 'light');
</script>

4. Comienza con la Interfaz

Define cómo se usará un composable antes de implementarlo^4. Esta es una forma de desarrollo "primero en diseño" que te ayuda a aclarar el propósito, entradas y salidas del composable antes de escribir código.

Consejo: Define las entradas (props, opciones) y salidas (valores retornados) del composable primero. Esto te ayuda a enfocarte en la API del composable.

Ejemplo:

// Antes de la Implementación: useCounter.js
// Debe aceptar un valor inicial
// Debe devolver un conteo y métodos para incrementar y decrementar
// Implementación:
import { ref } from 'vue';

export function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return { count, increment, decrement };
}

5. Reutilizando Lógica con Scoped Slots

Usa scoped slots para reutilizar lógica entre componentes de una manera única^5. Los scoped slots permiten que un componente padre pase datos y lógica a su hijo, proporcionando una forma flexible de compartir funcionalidad.

Consejo: Los scoped slots se pueden usar para pasar datos y lógica del componente padre al componente hijo. Esto permite que el componente hijo represente su contenido basado en los datos y lógica proporcionados por el padre.

Ejemplo:

<!-- DataFetcher.vue -->
<template>
  <div>
    <div v-if="loading">Cargando...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <slot :data="data" :loading="loading" :error="error"></slot>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue';

const props = defineProps({
  url: {
    type: String,
    required: true
  }
});

const data = ref(null);
const loading = ref(true);
const error = ref(null);

onMounted(async () => {
  try {
    const response = await fetch(props.url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    data.value = await response.json();
  } catch (e) {
    error.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>
<!-- Uso en un componente -->
<DataFetcher url="/api/items">
  <template v-slot="{ data, loading, error }">
    <div v-if="loading">Cargando elementos...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <ul>
        <li v-for="item in data" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>
  </template>
</DataFetcher>

Consejos Generales y Mejores Prácticas

1. Ref vs. Reactive

Entiende las diferencias entre ref y reactive y elige el apropiado para tu caso de uso^1. ref se usa para valores primitivos, mientras que reactive se usa para objetos y arrays.

Consejo: Usa ref para valores primitivos y reactive para objetos y arrays. Esto ayuda con el sistema de reactividad de Vue.

Ejemplo:

<script>
import { ref, reactive } from 'vue';

export default {
  setup() {
    const count = ref(0); // ref para un valor primitivo
    const user = reactive({ name: 'John', age: 30 }); // reactive para un objeto
    return { count, user };
  }
}
</script>

2. Gestión de Estado Efectiva

Estructura el estado en tus aplicaciones de manera efectiva^1. Esto es crucial para gestionar el flujo de datos y asegurar que tu aplicación permanezca manejable a medida que crece. Considera bibliotecas de gestión de estado apropiadas para aplicaciones más grandes.

Consejo: Considera usar Pinia o Vuex para gestión de estado en aplicaciones más grandes. Estas bibliotecas proporcionan gestión de estado centralizada y facilitan el manejo de estado de aplicación complejo.

Ejemplo: (Ilustrativo - la implementación actual depende de la biblioteca elegida)

// Ejemplo de Pinia (Conceptual)
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    isLoggedIn: false,
    user: null
  }),
  actions: {
    login(userData) {
      this.isLoggedIn = true;
      this.user = userData;
    },
    logout() {
      this.isLoggedIn = false;
      this.user = null;
    }
  }
});

3. Usa Comillas para Observar Valores Anidados

Observa valores anidados directamente usando comillas^2. Esto te permite observar cambios en propiedades específicas dentro de un objeto, activando actualizaciones cuando esas propiedades cambian. *Consejo: Usa comillas en la opción watch para observar propiedades anidadas de un objeto. Esta es una forma más eficiente y específica de monitorear cambios.

Ejemplo:

<script>
import { ref, watch } from 'vue';

export default {
  setup() {
    const data = ref({ user: { name: 'John' } });

    watch(() => data.value.user.name, (newName) => {
      console.log('Nombre cambió:', newName);
    });

    return { data };
  }
}
</script>

4. El Patrón de Condicional Extraído

Divide componentes basados en lógica condicional^3. Esto mejora la legibilidad y mantenibilidad separando preocupaciones. Hace que los componentes sean más fáciles de entender y probar.

Consejo: Si un componente tiene lógica condicional compleja, considera dividirlo en componentes más pequeños. Esto crea componentes más enfocados y manejables.

Ejemplo:

<!-- Antes: Lógica Condicional Compleja -->
<template>
  <div>
    <div v-if="isLoading">Cargando...</div>
    <div v-else-if="error">Error: {{ error }}</div>
    <div v-else>
      <UserList v-if="users.length > 0" :users="users" />
      <NoUsersMessage v-else />
    </div>
  </div>
</template>
<!-- Después: Usando Componentes Extraídos -->
<template>
  <LoadingIndicator v-if="isLoading" />
  <ErrorMessage v-if="error" :message="error" />
  <UsersDisplay v-else :users="users" />
</template>

5. 6 Razones para Dividir Componentes

Divide componentes en piezas más pequeñas para mejorar la organización del código y la reutilización^3. Esto aumenta la legibilidad, mantenibilidad y capacidad de prueba de tu código.

Consejo: Los componentes más pequeños son más fáciles de entender, probar y mantener. También promueven reutilización, ya que puedes usarlos en múltiples partes de tu aplicación.

Ejemplo:

<!-- Antes: Componente Monolítico -->
<template>
  <div>
    <Header />
    <Sidebar />
    <MainContent />
    <Footer />
  </div>
</template>
<!-- Después: Usando Componentes Extraídos -->
<template>
  <AppLayout>
    <template v-slot:header><Header /></template>
    <template v-slot:sidebar><Sidebar /></template>
    <template v-slot:main><MainContent /></template>
    <template v-slot:footer><Footer /></template>
  </AppLayout>
</template>

6. No Anules CSS de Componentes

Evita modificar directamente el CSS de un componente desde fuera del componente^2. Esto puede llevar a comportamiento inesperado y dificultar el mantenimiento de los estilos de tu aplicación. Encapsula estilos dentro del componente mismo.

Consejo: Usa props o slots para personalizar la apariencia de un componente. Esto permite estilos controlados y previene conflictos de estilos inesperados.

Ejemplo:

<!-- Button.vue -->
<template>
  <button :class="['button', variant]" @click="emit('click')">
    <slot></slot>
  </button>
</template>
<script setup>
const props = defineProps({
  variant: {
    type: String,
    default: 'primary'
  }
});

const emit = defineEmits(['click']);
</script>
<style scoped>
.button {
  /* Estilos base del botón */
}
.primary {
  /* Estilos del botón primario */
}
.secondary {
  /* Estilos del botón secundario */
}
</style>