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

Patrones de Componentes y Composables de Vue

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>

Turn the best models into shipped work

Teamday installs AI employees with the right model, harness, MCP servers, workspace files, review path, and recurring mission. Stop comparing tools in isolation and put them to work.