Desarrollo de Componentes Reutilizables y Extendibles en Vue 3 con Slots y Composables
Este tutorial te guiará a través del diseño y la implementación de componentes Vue 3 que son verdaderamente reutilizables y fáciles de extender. Exploraremos el poder de los slots para inyectar contenido dinámico y los composables para encapsular lógica de estado reactiva, facilitando la creación de aplicaciones mantenibles y escalables.
¡Hola, desarrolladores de Vue! 👋 En el mundo del desarrollo de aplicaciones, la reutilización de código es una piedra angular para la eficiencia y la mantenibilidad. Vue.js, con su enfoque en componentes, nos ofrece herramientas poderosas para lograrlo. Sin embargo, no basta con crear un componente; debemos asegurarnos de que sea flexible y adaptable a diferentes contextos sin caer en la rigidez.
En este tutorial, profundizaremos en dos conceptos clave que elevan la reutilización y extensibilidad de tus componentes Vue 3 a un nuevo nivel: los Slots y los Composables. Aprenderás no solo a usarlos, sino a aplicarlos estratégicamente para construir una arquitectura de componentes robusta y fácil de escalar.
🎯 ¿Por Qué la Reutilización y la Extensibilidad Son Clave?
Imagina que estás construyendo una aplicación y necesitas un botón en varios lugares. Al principio, podrías simplemente copiar y pegar el código HTML y CSS. Pero, ¿qué pasa si necesitas cambiar el color o el tamaño? Tendrías que modificarlo en todos los lugares. Esto es ineficiente y propenso a errores.
Ahí es donde entran los componentes. Un componente encapsula lógica, estilos y estructura, permitiéndote usarlo múltiples veces. Pero incluso los componentes básicos pueden volverse limitados. ¿Qué pasa si necesitas que tu botón muestre un icono diferente o que ejecute una lógica específica solo en un caso particular? Aquí es donde la extensibilidad brilla.
Beneficios Clave:
- Eficiencia: Escribe menos código, haz más.
- Mantenibilidad: Un cambio en un componente se propaga a todos sus usos.
- Consistencia: Uniformidad visual y de comportamiento en toda la aplicación.
- Escalabilidad: Añade nuevas funcionalidades sin afectar las existentes.
📖 Entendiendo los Slots en Vue 3
Los slots son una de las características más potentes de Vue.js para la composición de componentes. Permiten que los componentes padre inyecten contenido en el template de los componentes hijo. Piensa en ellos como placeholders que el componente hijo expone para que el padre pueda rellenar.
Slot Básico o Por Defecto
El slot más simple se define con la etiqueta <slot /> dentro del template del componente hijo. Cualquier contenido que pases al componente hijo sin especificar un nombre de slot, irá a este slot por defecto.
Componente BaseCard.vue:
<template>
<div class="base-card">
<div class="card-header">
<h3><slot name="header">Título por defecto</slot></h3>
</div>
<div class="card-body">
<slot>Contenido por defecto de la tarjeta</slot>
</div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped>
.base-card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.card-header {
border-bottom: 1px solid #eee;
padding-bottom: 8px;
margin-bottom: 12px;
}
.card-header h3 {
margin: 0;
color: #333;
}
.card-body {
color: #555;
line-height: 1.6;
}
.card-footer {
border-top: 1px solid #eee;
padding-top: 8px;
margin-top: 12px;
text-align: right;
}
</style>
Uso en el Componente Padre:
<template>
<div>
<h2>Ejemplo de Slot Básico</h2>
<BaseCard>
<p>Este es el contenido principal de la tarjeta. ¡Qué fácil es!</p>
</BaseCard>
</div>
</template>
<script setup>
import BaseCard from './components/BaseCard.vue';
</script>
En este ejemplo, el párrafo Este es el contenido principal... se inyectará en el slot sin nombre (<slot>) dentro de BaseCard.
Slots Nombrados (Named Slots) ✨
Cuando necesitas inyectar contenido en diferentes áreas del componente hijo, los slots nombrados son la solución. Se definen con el atributo name en la etiqueta <slot />.
Componente BaseCard.vue (ejemplo anterior, ya incluye slots nombrados):
<!-- ... dentro de BaseCard.vue ... -->
<div class="card-header">
<h3><slot name="header">Título por defecto</slot></h3>
</div>
<!-- ... -->
<div class="card-footer">
<slot name="footer"></slot>
</div>
<!-- ... -->
Uso en el Componente Padre:
<template>
<div>
<h2>Ejemplo de Slots Nombrados</h2>
<BaseCard>
<!-- Contenido para el slot 'header' -->
<template #header>
<h3>🚀 Mi Tarjeta Personalizada</h3>
</template>
<!-- Contenido para el slot por defecto -->
<p>Aquí va un párrafo con información importante dentro del cuerpo de la tarjeta.</p>
<template #footer>
<button class="btn primary">Ver más</button>
<button class="btn secondary">Cerrar</button>
</template>
</BaseCard>
</div>
</template>
<script setup>
import BaseCard from './components/BaseCard.vue';
</script>
<style scoped>
.btn {
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 8px;
}
.btn.primary {
background-color: #4CAF50;
color: white;
border: none;
}
.btn.secondary {
background-color: #f44336;
color: white;
border: none;
}
</style>
Aquí, usamos la sintaxis <template #nombreDelSlot> (abreviatura de v-slot:nombreDelSlot) para indicar qué contenido debe ir en qué slot nombrado.
Slots con Ámbito (Scoped Slots) 🌐
Los scoped slots son la característica más avanzada y flexible de los slots. Permiten que el componente hijo pase datos al componente padre, donde el padre puede usar esos datos para renderizar el contenido del slot. Esto es increíblemente útil cuando el componente hijo es responsable de gestionar algunos datos, pero el padre quiere controlar cómo se presentan esos datos.
Para pasar datos desde un slot hijo, se usa el v-bind en la etiqueta <slot />.
Componente UserProfileCard.vue:
<template>
<div class="user-profile-card">
<slot :user="user" :level="level" :hasBadge="user.reputation > 100">
<!-- Contenido de fallback si el padre no proporciona un slot -->
<div class="default-profile">
<h4>{{ user.name }}</h4>
<p>Email: {{ user.email }}</p>
<span v-if="user.reputation > 100" class="badge blue">Pro</span>
</div>
</slot>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const user = ref({
name: 'Jane Doe',
email: 'jane.doe@example.com',
reputation: 120
});
const level = ref('Experto');
onMounted(() => {
// Simular una carga de datos
setTimeout(() => {
user.value.name = 'Alice Smith';
user.value.email = 'alice.smith@example.com';
user.value.reputation = 250;
level.value = 'Maestro';
}, 2000);
});
</script>
<style scoped>
.user-profile-card {
border: 1px solid #ddd;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
background-color: #f9f9f9;
text-align: center;
}
.default-profile h4 {
color: #2c3e50;
margin-bottom: 5px;
}
.default-profile p {
color: #7f8c8d;
font-size: 0.9em;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
font-weight: bold;
color: white;
margin-top: 10px;
}
.badge.blue {
background-color: #3498db;
}
</style>
Uso en el Componente Padre:
<template>
<div>
<h2>Ejemplo de Scoped Slots</h2>
<UserProfileCard v-slot="slotProps">
<div class="custom-profile">
<img src="https://via.placeholder.com/60" alt="Avatar" class="avatar" />
<h3>{{ slotProps.user.name }} ({{ slotProps.level }})</h3>
<p>{{ slotProps.user.email }}</p>
<span v-if="slotProps.hasBadge" class="badge green">Verificado</span>
<p class="reputation">Reputación: {{ slotProps.user.reputation }}</p>
</div>
</UserProfileCard>
<UserProfileCard>
<!-- Usará el contenido por defecto del slot -->
</UserProfileCard>
</div>
</template>
<script setup>
import UserProfileCard from './components/UserProfileCard.vue';
</script>
<style scoped>
.custom-profile {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar {
border-radius: 50%;
margin-bottom: 10px;
border: 2px solid #3498db;
}
.custom-profile h3 {
color: #2980b9;
margin: 5px 0;
}
.custom-profile p {
color: #34495e;
font-style: italic;
}
.reputation {
font-weight: bold;
color: #27ae60;
}
.badge.green {
background-color: #2ecc71;
}
</style>
En el componente padre, usamos v-slot="slotProps" para capturar todas las propiedades que el slot hijo expone. Luego, podemos acceder a ellas como slotProps.user, slotProps.level, etc.
🛠️ Creando Composables Reutilizables en Vue 3
Mientras que los slots se enfocan en la reutilización de la estructura y el contenido, los Composables se centran en la reutilización de la lógica con estado reactivo.
Un composable es simplemente una función que aprovecha la API de composición de Vue para encapsular lógica reactiva y puede ser reutilizada entre componentes. Por convención, los composables suelen empezar con use... (ej: useCounter, useToggle).
Caso de Uso: Gestor de Paginación
Imagina que necesitas implementar paginación en varias tablas de tu aplicación. En lugar de copiar y pegar la lógica de paginación (estado de la página actual, número total de elementos, cálculo del rango, etc.) en cada componente, puedes crear un composable.
1. Crear el Composable usePagination.js:
// composables/usePagination.js
import { ref, computed } from 'vue';
export function usePagination(itemsPerPage = 10) {
const currentPage = ref(1);
const totalItems = ref(0);
const totalPages = computed(() => {
if (totalItems.value === 0) return 1;
return Math.ceil(totalItems.value / itemsPerPage);
});
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * itemsPerPage;
const end = start + itemsPerPage;
// Este composable no conoce los ítems, solo la lógica de paginación.
// Los ítems se le pasarán desde el componente.
return { start, end };
});
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page;
}
};
const nextPage = () => {
goToPage(currentPage.value + 1);
};
const prevPage = () => {
goToPage(currentPage.value - 1);
};
const setTotalItems = (count) => {
totalItems.value = count;
// Resetear a la primera página si el total de ítems cambia y la página actual es inválida
if (currentPage.value > totalPages.value) {
currentPage.value = 1;
}
};
return {
currentPage,
totalPages,
paginatedItems,
goToPage,
nextPage,
prevPage,
setTotalItems,
itemsPerPage // También podemos exponer la configuración
};
}
2. Usar el Composable en un Componente:
<template>
<div class="data-table">
<h2>Listado de Productos 🛒</h2>
<div class="search-bar">
<input type="text" v-model="searchTerm" placeholder="Buscar producto..." @input="applySearch" />
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio</th>
<th>Categoría</th>
</tr>
</thead>
<tbody>
<tr v-for="product in displayedProducts" :key="product.id">
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>€{{ product.price.toFixed(2) }}</td>
<td>{{ product.category }}</td>
</tr>
<tr v-if="displayedProducts.length === 0">
<td colspan="4">No hay productos que coincidan con la búsqueda.</td>
</tr>
</tbody>
</table>
<div class="pagination-controls" v-if="totalPages > 1">
<button @click="prevPage" :disabled="currentPage === 1">Anterior</button>
<span class="page-info">Página {{ currentPage }} de {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">Siguiente</button>
<select v-model.number="itemsPerPage" @change="handleItemsPerPageChange">
<option :value="5">5 por página</option>
<option :value="10">10 por página</option>
<option :value="20">20 por página</option>
</select>
</div>
</div>
</template>
<script setup>
import { ref, watch, computed, onMounted } from 'vue';
import { usePagination } from './composables/usePagination'; // Asegúrate de que la ruta sea correcta
// Datos de ejemplo
const allProducts = ref([
{ id: 1, name: 'Laptop Pro', price: 1200.00, category: 'Electrónica' },
{ id: 2, name: 'Smartphone Ultra', price: 899.99, category: 'Electrónica' },
{ id: 3, name: 'Teclado Mecánico', price: 95.50, category: 'Accesorios' },
{ id: 4, name: 'Mouse Inalámbrico', price: 30.00, category: 'Accesorios' },
{ id: 5, name: 'Monitor 27 Pulgadas', price: 350.00, category: 'Electrónica' },
{ id: 6, name: 'Disco Duro SSD 1TB', price: 110.00, category: 'Almacenamiento' },
{ id: 7, name: 'Webcam HD', price: 60.00, category: 'Accesorios' },
{ id: 8, name: 'Auriculares Gaming', price: 150.00, category: 'Audio' },
{ id: 9, name: 'Router Wi-Fi 6', price: 130.00, category: 'Redes' },
{ id: 10, name: 'Impresora Multifunción', price: 200.00, category: 'Oficina' },
{ id: 11, name: 'Tableta Gráfica', price: 280.00, category: 'Diseño' },
{ id: 12, name: 'Cargador Universal', price: 25.00, category: 'Accesorios' },
{ id: 13, name: 'Software Antivirus', price: 50.00, category: 'Software' },
{ id: 14, name: 'Altavoces Bluetooth', price: 75.00, category: 'Audio' },
{ id: 15, name: 'Mochila para Laptop', price: 40.00, category: 'Accesorios' },
{ id: 16, name: 'Smartwatch', price: 180.00, category: 'Wearables' }
]);
const searchTerm = ref('');
const filteredProducts = ref(allProducts.value);
// Inicializar el composable de paginación
const {
currentPage,
totalPages,
paginatedItems,
goToPage,
nextPage,
prevPage,
setTotalItems,
itemsPerPage // Importamos también itemsPerPage para el select
} = usePagination(10); // Inicialmente 10 ítems por página
// Observar cambios en filteredProducts para actualizar el total de ítems en la paginación
watch(filteredProducts, (newVal) => {
setTotalItems(newVal.length);
}, { immediate: true });
// Productos a mostrar en la página actual después de filtrar y paginar
const displayedProducts = computed(() => {
const { start, end } = paginatedItems.value;
return filteredProducts.value.slice(start, end);
});
const applySearch = () => {
const term = searchTerm.value.toLowerCase();
if (term) {
filteredProducts.value = allProducts.value.filter(product =>
product.name.toLowerCase().includes(term) |
product.category.toLowerCase().includes(term)
);
} else {
filteredProducts.value = allProducts.value;
}
// Después de filtrar, volvemos a la primera página
goToPage(1);
};
const handleItemsPerPageChange = () => {
// Cuando itemsPerPage cambia a través del select, actualizamos el composable
// y forzamos un reinicio para que recalcule las páginas
// Aquí, como itemsPerPage es una ref del composable, se actualizará directamente.
// Pero es buena práctica llamar a setTotalItems nuevamente por si el total de ítems
// actual genera menos páginas y la actual es inválida.
setTotalItems(filteredProducts.value.length);
goToPage(1);
};
onMounted(() => {
// Asegúrate de que el total de ítems esté inicializado correctamente al montar el componente
setTotalItems(allProducts.value.length);
});
</script>
<style scoped>
.data-table {
margin: 20px;
font-family: Arial, sans-serif;
}
.search-bar {
margin-bottom: 20px;
}
.search-bar input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table th, table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
table th {
background-color: #f2f2f2;
font-weight: bold;
}
table tbody tr:nth-child(even) {
background-color: #f9f9f9;
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.pagination-controls button {
padding: 8px 12px;
border: 1px solid #007bff;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
}
.pagination-controls button:disabled {
background-color: #cccccc;
border-color: #cccccc;
cursor: not-allowed;
}
.page-info {
font-weight: bold;
}
.pagination-controls select {
padding: 7px;
border-radius: 4px;
border: 1px solid #ddd;
}
</style>
En este ejemplo, usePagination encapsula toda la lógica de paginación. El componente DataTable solo tiene que importar y usar esta lógica, pasándole el totalItems y controlando qué porción de sus propios datos mostrar. Esto mantiene la lógica de paginación separada de la lógica específica de la tabla de productos, haciendo que ambas sean más mantenibles y reutilizables.
Ventajas de los Composables:
- Reutilización de Lógica: Evita la duplicación de código.
- Organización: Mantiene el código de los componentes más limpio y enfocado.
- Probabilidad: Es más fácil probar la lógica encapsulada en un composable que dentro de un componente complejo.
- Flexibilidad: Adapta la lógica a diferentes componentes pasándoles argumentos.
🔄 Combinando Slots y Composables: Sinergia para Componentes Avanzados
El verdadero poder surge cuando combinamos estas dos herramientas. Imagina un componente AdvancedFilterTable que no solo necesita lógica de paginación, sino también lógica de filtrado y ordenación. Podríamos tener composables como useFilter y useSort.
Además, este AdvancedFilterTable podría usar scoped slots para permitir que el padre defina cómo se renderizan las filas de la tabla o los controles de filtro, mientras el componente de tabla gestiona la lógica subyacente de los datos.
Ejemplo: SortableTable con Composable de Ordenación y Scoped Slot para Filas
1. Composable useSort.js:
// composables/useSort.js
import { ref, computed } from 'vue';
export function useSort(initialSortKey, initialSortOrder = 'asc') {
const sortKey = ref(initialSortKey);
const sortOrder = ref(initialSortOrder);
const sortedItems = computed(() => (items) => {
if (!sortKey.value) return items;
// Clonar para no mutar el array original
const sortableItems = [...items];
return sortableItems.sort((a, b) => {
const aValue = a[sortKey.value];
const bValue = b[sortKey.value];
if (aValue === bValue) return 0;
let comparison = 0;
if (typeof aValue === 'string' && typeof bValue === 'string') {
comparison = aValue.localeCompare(bValue);
} else {
comparison = aValue < bValue ? -1 : 1;
}
return sortOrder.value === 'asc' ? comparison : -comparison;
});
});
const sortBy = (key) => {
if (sortKey.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
sortKey.value = key;
sortOrder.value = 'asc';
}
};
const getSortIcon = (key) => {
if (sortKey.value !== key) return '';
return sortOrder.value === 'asc' ? '▲' : '▼';
}
return {
sortKey,
sortOrder,
sortedItems,
sortBy,
getSortIcon
};
}
2. Componente SortableTable.vue:
<template>
<div class="sortable-table-container">
<table>
<thead>
<tr>
<th v-for="header in headers" :key="header.key" @click="sortBy(header.key)">
{{ header.label }} <span v-if="getSortIcon(header.key)">{{ getSortIcon(header.key) }}</span>
</th>
</tr>
</thead>
<tbody>
<template v-if="sortedData.length > 0">
<slot name="row" v-for="item in sortedData" :item="item" :key="item.id"></slot>
</template>
<tr v-else>
<td :colspan="headers.length">No hay datos disponibles.</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useSort } from './composables/useSort';
const props = defineProps({
data: {
type: Array,
required: true
},
headers: {
type: Array,
required: true,
validator: (value) => value.every(h => h.key && h.label)
},
initialSortKey: String,
initialSortOrder: {
type: String,
default: 'asc',
validator: (value) => ['asc', 'desc'].includes(value)
}
});
const { sortKey, sortOrder, sortedItems, sortBy, getSortIcon } = useSort(
props.initialSortKey || (props.headers.length > 0 ? props.headers[0].key : null),
props.initialSortOrder
);
const sortedData = computed(() => {
return sortedItems.value(props.data);
});
</script>
<style scoped>
.sortable-table-container {
margin: 20px;
font-family: Arial, sans-serif;
}
table {
width: 100%;
border-collapse: collapse;
}
table th, table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
table th {
background-color: #eaf6ff;
font-weight: bold;
cursor: pointer;
user-select: none;
color: #333;
}
table th:hover {
background-color: #dbeeff;
}
table tbody tr:nth-child(even) {
background-color: #f6faff;
}
</style>
3. Uso en un Componente Padre:
<template>
<div>
<h2>Usuarios de la Aplicación</h2>
<SortableTable :data="users" :headers="userHeaders" initial-sort-key="name">
<template #row="{ item: user }">
<tr>
<td>{{ user.id }}</td>
<td><span class="user-name">{{ user.name }}</span></td>
<td>{{ user.email }}</td>
<td><span :class="['status-badge', user.isActive ? 'active' : 'inactive']">{{ user.isActive ? 'Activo' : 'Inactivo' }}</span></td>
<td>{{ user.role }}</td>
<td>
<button class="action-btn view-btn">Ver</button>
<button class="action-btn edit-btn">Editar</button>
</td>
</tr>
</template>
</SortableTable>
</div>
</template>
<script setup>
import { ref } from 'vue';
import SortableTable from './components/SortableTable.vue';
const users = ref([
{ id: 1, name: 'Carlos Gomez', email: 'carlos@example.com', isActive: true, role: 'Admin' },
{ id: 2, name: 'Ana Lopez', email: 'ana@example.com', isActive: false, role: 'Editor' },
{ id: 3, name: 'Maria Perez', email: 'maria@example.com', isActive: true, role: 'Viewer' },
{ id: 4, name: 'Juan Rodriguez', email: 'juan@example.com', isActive: true, role: 'Admin' },
{ id: 5, name: 'Laura Martinez', email: 'laura@example.com', isActive: false, role: 'Viewer' },
{ id: 6, name: 'Pedro Sanchez', email: 'pedro@example.com', isActive: true, role: 'Editor' }
]);
const userHeaders = ref([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Nombre' },
{ key: 'email', label: 'Email' },
{ key: 'isActive', label: 'Estado' },
{ key: 'role', label: 'Rol' },
{ key: 'actions', label: 'Acciones' }
]);
</script>
<style scoped>
.user-name {
font-weight: bold;
color: #007bff;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 5px;
font-size: 0.8em;
color: white;
}
.status-badge.active {
background-color: #28a745;
}
.status-badge.inactive {
background-color: #dc3545;
}
.action-btn {
padding: 6px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
font-size: 0.9em;
}
.action-btn.view-btn {
background-color: #17a2b8;
color: white;
}
.action-btn.edit-btn {
background-color: #ffc107;
color: #333;
}
</style>
En este complejo ejemplo:
- El
useSortcomposable gestiona toda la lógica de ordenación (clave actual, dirección, función de ordenación, icono). - El componente
SortableTablerecibe los datos y las cabeceras, y usauseSortpara obtener los datos ya ordenados. - Pero la representación de cada fila no la define
SortableTable. En su lugar, usa unscoped slot(#row="{ item: user }") para pasar cadaitem(usuario) al componente padre, dejando que el padre decida cómo renderizar las celdas y añadir botones de acción, etc.
Esto nos da un componente de tabla altamente reutilizable (la lógica de ordenación está desacoplada y la estructura de la tabla es genérica) y al mismo tiempo extremadamente extendible (el padre tiene control total sobre el contenido de las filas).
💡 Buenas Prácticas y Consideraciones
Al trabajar con slots y composables, ten en cuenta las siguientes buenas prácticas:
Para Slots:
- Nombres Descriptivos: Usa nombres de slots claros para que sea evidente dónde se inyectará el contenido.
- Contenido de Fallback: Proporciona contenido por defecto para slots. Esto hace que el componente sea más robusto si el padre no inyecta nada.
- Documentación: Si tu componente tiene muchos slots, documéntalos claramente para otros desarrolladores (o para tu yo futuro).
- No Abuses: Demasiados slots pueden hacer que un componente sea difícil de entender. Si un componente tiene una enorme cantidad de slots, podría ser una señal de que está haciendo demasiadas cosas y necesita ser descompuesto.
Para Composables:
- Convención de Nombres: Nombra tus composables con
useprefijo (ej:useForm,useLocalStorage). - Estado Reactivo: Asegúrate de que los composables devuelvan Refs o Reactives para que la lógica sea interactiva con Vue.
- Separación de Preocupaciones: Cada composable debe encargarse de una única responsabilidad bien definida.
- Parámetros Configurable: Permite que los composables acepten parámetros para configurarlos (ej:
itemsPerPageenusePagination). - Pruebas Unitarias: Los composables son inherentemente más fáciles de probar de forma unitaria, ¡aprovéchalo!
Sinergia:
- Identifica Oportunidades: Busca patrones. ¿Hay lógica reactiva que se repite? -> ¡Composable! ¿Hay estructura de componente que debe ser flexible? -> ¡Slots!
- Componibilidad Primero: Diseña tus componentes pensando en cómo pueden ser combinados y extendidos por otros.
Conclusión ✅
Dominar los slots y los composables es fundamental para escribir código Vue 3 elegante, eficiente y mantenible. Los slots te dan la capacidad de componer la interfaz de usuario de manera flexible, mientras que los composables te permiten reutilizar la lógica reactiva de una forma limpia y desacoplada.
Al integrar estas herramientas en tu flujo de trabajo, no solo mejorarás la calidad de tu código, sino que también acelerarás el desarrollo de tus aplicaciones y facilitarás la colaboración en equipo. ¡Ahora estás equipado para construir componentes Vue que son verdaderamente potentes y adaptables!
Tutoriales relacionados
- Optimización de Rendimiento en Vue.js 3: Estrategias Avanzadas para Aplicaciones Rápidasintermediate15 min
- Navegación Dinámica en Vue Router: Rutas Anidadas y Parámetros Avanzadosintermediate20 min
- Domina la Reactividad: Explorando Refs y Reactive en Vue 3 para una Gestión de Estado Eficienteintermediate18 min
- Gestión de Estado Centralizada con Pinia en Vue 3: Guía Completaintermediate18 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!