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
refpara valores primitivos yreactivepara 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>

