Siete técnicas avanzadas de Vue 3 que conviene conocer
Composables, Teleport, Suspense, directivas personalizadas, funciones de render, provide/inject y plugins, cada uno con su código.
Vue 3 lleva tiempo formando parte del trabajo diario de frontend. Es intuitivo, resulta agradable construir con él y lo básico te lleva bastante lejos. Lo que sigue está más allá de ese punto: siete técnicas que me han resultado útiles en mis propios proyectos.
1. La Composition API más allá de setup()
Puedes objetar que la Composition API ya no es algo avanzado, y es justo. Casi todos nos manejamos con setup() y las referencias reactivas. La parte que suele quedarse sin usar es extraer lógica a composables.
Piensa en un composable como una función que encapsula lógica con estado. Los componentes adelgazan porque solo se ocupan del render, la lógica se agrupa por funcionalidad en lugar de por opción, y esa misma lógica se comparte sin los problemas que causaban los mixins.
Supón que tienes una funcionalidad que consulta datos y controla los estados de carga y error. En vez de repetirla en cada componente:
// composables/useFetch.js
import { ref, onMounted } from "vue";
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(true);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
data.value = await res.json();
} catch (e) {
error.value = e;
} finally {
loading.value = false;
}
}
onMounted(fetchData);
return { data, error, loading, fetchData };
}
Después cualquier componente puede usarla:
<template>
<div>
<div v-if="loading">Loading posts...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
<script setup>
import { useFetch } from "@/composables/useFetch";
const { data, error, loading } = useFetch(
"https://jsonplaceholder.typicode.com/posts"
);
</script>
2. Teleport, para elementos que necesitan escapar
Si has peleado con guerras de z-index al renderizar modales, tooltips o notificaciones desde un componente muy anidado, la respuesta es Teleport. Renderiza una parte de tu plantilla en otro punto del DOM, fuera de la jerarquía del componente, manteniendo la reactividad con su estado.
Encaja con modales y diálogos, avisos y notificaciones, tooltips y desplegables.
<!-- In your App.vue or main layout file -->
<body>
<div id="app"></div>
<div id="modals-container"></div>
<!-- This is our teleport target -->
</body>
<!-- components/MyModal.vue -->
<template>
<Teleport to="#modals-container">
<div v-if="isOpen" class="modal-backdrop" @click="close">
<div class="modal-content" @click.stop>
<h3>My Awesome Modal</h3>
<p>This modal content is rendered outside the component!</p>
<button @click="close">Close</button>
</div>
</div>
</Teleport>
</template>
<script setup>
import { ref } from "vue";
const isOpen = ref(false);
const open = () => (isOpen.value = true);
const close = () => (isOpen.value = false);
// Expose open/close for parent component to use
defineExpose({ open, close });
</script>
<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 300px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
Se use donde se use MyModal, su contenido acaba dentro de #modals-container, lo que saca los conflictos de estilos de la ecuación.
3. Suspense para estados de carga asíncronos
Esperar datos o componentes cargados de forma asíncrona hace que la interfaz se sienta lenta. Suspense coordina las dependencias asíncronas de un árbol de componentes y muestra un contenido de respaldo mientras se resuelven, de modo que el usuario ve un estado de carga en lugar de un hueco vacío o un diseño roto.
Un componente que consulta datos al montarse:
<!-- components/AsyncDataDisplay.vue -->
<template>
<div>
<h2>Data from API:</h2>
<p>{{ data.title }}</p>
</div>
</template>
<script setup>
import { ref } from "vue";
const data = ref(null);
// Simulate an async data fetch
await new Promise((resolve) => setTimeout(resolve, 2000));
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
data.value = await res.json();
</script>
Envuelto en Suspense:
<template>
<div>
<h1>My App</h1>
<Suspense>
<!-- Main content that might be async -->
<AsyncDataDisplay />
<!-- Fallback content while async components are loading -->
<template #fallback>
<div>Loading awesome data...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent } from "vue";
// Define an async component
const AsyncDataDisplay = defineAsyncComponent(
() => import("./components/AsyncDataDisplay.vue")
);
</script>
Mientras AsyncDataDisplay resuelve el await de su bloque setup, se ve el contenido de respaldo. Cuando termina, el contenido real ocupa su lugar.
4. Directivas personalizadas para trabajar con el DOM
A veces la herramienta adecuada no es un componente. Para manipulación de bajo nivel del DOM, o para comportamientos reutilizables que no dependen de un componente concreto, las directivas personalizadas te dan acceso directo al elemento al que se asocian. Cosas como v-focus, v-tooltip o v-lazy-load.
// main.js or a dedicated directives file
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("focus", {
mounted(el) {
el.focus();
},
});
app.mount("#app");
Se usa así:
<template>
<input v-focus type="text" placeholder="I will be focused on mount" />
</template>
El input recibe el foco cuando el componente se monta. Las directivas pueden engancharse a created, mounted, updated y unmounted para controlar el comportamiento durante el ciclo de vida del elemento.
5. Funciones de render y JSX
Para casi todo, la sintaxis de plantillas de Vue es la opción correcta: declarativa, legible y se compila de forma eficiente. Las funciones de render y JSX son para los casos raros en los que necesitas control programático sobre el render, como un generador de tablas muy dinámico o un componente de biblioteca cuya estructura cambia según props complejas.
Probablemente no recurras a esto salvo que la estructura varíe mucho, estés escribiendo una biblioteca de interfaz que necesita máxima flexibilidad o prefieras escribir los componentes enteramente en JavaScript o TypeScript.
// components/DynamicHeading.vue
import { h } from "vue";
export default {
props: {
level: {
type: Number,
required: true,
validator: (val) => val >= 1 && val <= 6,
},
},
setup(props) {
return () => h(`h${props.level}`, `This is a level ${props.level} heading`);
},
};
Lo mismo con JSX, que requiere la configuración de Babel para JSX en Vue:
// components/DynamicHeadingJSX.vue
export default {
props: {
level: {
type: Number,
required: true,
validator: (val) => val >= 1 && val <= 6,
},
},
setup(props) {
const HeadingTag = `h${props.level}`;
return () => (
<HeadingTag>This is a level {props.level} heading (with JSX)</HeadingTag>
);
},
};
6. Provide e inject
Para estado realmente global, la respuesta es Pinia. Provide e inject cubren el caso intermedio: datos o utilidades que deben llegar al fondo de un árbol de componentes sin pasar props por cada nivel.
Encajan con la información de tema, las preferencias de usuario, funciones auxiliares y datos que cambian poco pero afectan a todo un subárbol.
<!-- components/GrandparentComponent.vue -->
<template>
<div>
<h1>Grandparent</h1>
<button @click="toggleTheme">Toggle Theme</button>
<ChildComponent />
</div>
</template>
<script setup>
import { provide, ref } from "vue";
import ChildComponent from "./ChildComponent.vue";
const theme = ref("light");
const toggleTheme = () => {
theme.value = theme.value === "light" ? "dark" : "light";
};
provide("appTheme", theme); // Provide the reactive theme
provide("toggleThemeFunction", toggleTheme); // Provide a function too!
</script>
<!-- components/DeeplyNestedComponent.vue -->
<template>
<div :class="['card', appTheme === 'dark' ? 'dark-mode' : '']">
<p>Current theme: {{ appTheme }}</p>
<button @click="toggleThemeFunction">Toggle Theme (from deep)</button>
</div>
</template>
<script setup>
import { inject } from "vue";
const appTheme = inject("appTheme");
const toggleThemeFunction = inject("toggleThemeFunction"); // Inject the function
</script>
<style scoped>
.card {
border: 1px solid #ccc;
padding: 15px;
margin-top: 10px;
border-radius: 5px;
}
.dark-mode {
background-color: #333;
color: #eee;
border-color: #555;
}
</style>
DeeplyNestedComponent recibe el tema y la función sin que GrandparentComponent pase nada a través de ChildComponent.
7. Propiedades globales y plugins
A veces algo tiene que estar disponible en toda la aplicación: una instancia HTTP, una función de traducción, una utilidad compartida.
La vía más simple es app.config.globalProperties, que se engancha a this en la Options API y es accesible con getCurrentInstance() en la Composition API.
// main.js
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
// Attach a global property
app.config.globalProperties.$myGlobalUtil = {
sayHello: () => console.log("Hello from global util!"),
version: "1.0.0",
};
app.mount("#app");
En una plantilla eso es <button @click="$myGlobalUtil.sayHello()">Say Hello</button>, y en un bloque script setup:
<script setup>
import { getCurrentInstance } from "vue";
const { proxy } = getCurrentInstance();
proxy.$myGlobalUtil.sayHello();
console.log(proxy.$myGlobalUtil.version);
</script>
Para algo mayor, que implique varias propiedades globales, directivas, componentes o lógica de enrutado, escribe un plugin. Un plugin es una función que recibe la instancia app y un objeto opcional de opciones.
// plugins/myPlugin.js
export default {
install: (app, options) => {
// Make a global method available
app.config.globalProperties.$translate = (key) => {
return key
.split(".")
.reduce((o, i) => (o ? o[i] : undefined), options.translations);
};
// Provide a global composable
app.provide("globalStore", {
user: "John Doe",
settings: { theme: "dark" },
});
// Or register a global component
// app.component('MyGlobalComponent', MyGlobalComponent)
},
};
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import MyPlugin from "./plugins/myPlugin";
const app = createApp(App);
const translations = {
hello: "Bonjour",
welcome: {
message: "Bienvenue!",
},
};
app.use(MyPlugin, { translations }); // Use the plugin with options
app.mount("#app");
A partir de ahí cualquier componente puede llamar a $translate('welcome.message') o inyectar globalStore. Es el mismo mecanismo que usan Vue Router y Pinia para integrarse en una aplicación.
Estos siete patrones cubren la mayoría de las situaciones en las que lo básico deja de bastar. Cuanto más recurres a ellos, más naturales resultan.