13: Conoce y aprende a usar Inertia.js junto con Vue.js

 

Introducción

En este tutorial exploraremos dos tecnologías fundamentales de nuestro stack Bolt: Inertia.js y Vue.js. Estas herramientas trabajan en conjunto para crear aplicaciones de página única (SPA) manteniendo la simplicidad del desarrollo tradicional con Laravel.


¿Qué es Inertia.js?

Inertia.js es un puente que conecta el backend (Laravel) con el frontend (Vue.js) sin la complejidad típica de las APIs REST o GraphQL. En lugar de construir una API separada, Inertia te permite crear aplicaciones SPA usando los controladores y rutas de Laravel.

Características principales de Inertia:

  • Sin necesidad de API: Usa directamente los controladores de Laravel

  • SPA nativo: Navegación sin recargar la página

  • Historial automático: Maneja el historial del navegador

  • Progresivo: Puedes migrar gradualmente de Laravel tradicional

  • Rendimiento: Solo envía los datos necesarios

Analogía:

Vue.js es como PHP (el lenguaje de programación frontend)
Inertia.js es como Laravel (el framework que orquesta todo)


Paso 1: Conceptos fundamentales de Inertia

1.1 Renderizado de páginas

En lugar de devolver vistas Blade, los controladores usan Inertia::render():

php
// En un controlador de Laravel
use Inertia\Inertia;

public function index()
{
    return Inertia::render('Welcome', [
        'user' => auth()->user(),
        'skills' => Skill::all()
    ]);
}

1.2 Estructura de una página Vue

Cada página Inertia es un componente Vue que recibe props:

vue
<script setup>
// Definir las props que vienen del backend
defineProps({
    user: Object,
    skills: Array
});
</script>

<template>
    <div>
        <h1>Bienvenido {{ user?.name }}</h1>
        <div v-for="skill in skills" :key="skill.id">
            {{ skill.name }}
        </div>
    </div>
</template>

1.3 Navegación

Inertia proporciona el componente <Link> para navegación SPA:

vue
<template>
    <!-- Navegación sin recargar -->
    <Link href="/projects" class="nav-link">
        Proyectos
    </Link>
    
    <!-- Con datos adicionales -->
    <Link :href="route('projects.show', { id: 1 })">
        Ver Proyecto
    </Link>
</template>

<script setup>
import { Link } from '@inertiajs/vue3';
</script>

Paso 2: Componentes clave de Inertia

2.1 El componente <Head>

Permite establecer meta tags y título desde cualquier página:

vue
<template>
    <Head>
        <title>Mi Portafolio - Proyectos</title>
        <meta name="description" content="Mis proyectos más recientes" />
    </Head>
</template>

<script setup>
import { Head } from '@inertiajs/vue3';
</script>

2.2 El componente <Link>

Para navegación SPA con todas las funcionalidades:

vue
<template>
    <!-- Enlace básico -->
    <Link href="/about">Sobre mí</Link>
    
    <!-- Con datos de método POST -->
    <Link href="/logout" method="post" as="button">
        Cerrar Sesión
    </Link>
    
    <!-- Con parámetros -->
    <Link :href="route('projects.show', project.id)">
        {{ project.title }}
    </Link>
    
    <!-- Con estado de carga -->
    <Link :href="url" :preserve-scroll="true">
        Cargar más
    </Link>
</template>

<script setup>
import { Link } from '@inertiajs/vue3';
</script>

2.3 El hook usePage()

Accede a los datos compartidos globalmente:

vue
<script setup>
import { usePage } from '@inertiajs/vue3';

const page = usePage();
const user = page.props.auth.user;
const appName = page.props.appName;
</script>

Paso 3: Configuración del entorno

3.1 Verificar la instalación

Asegúrate de tener las dependencias correctas en package.json:

json
{
    "devDependencies": {
        "@inertiajs/vue3": "^1.0.0",
        "@vitejs/plugin-vue": "^4.0.0",
        "vue": "^3.2.0"
    }
}

3.2 Archivo de entrada (app.js)

El archivo resources/js/app.js inicializa Inertia:

javascript
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';

const appName = import.meta.env.VITE_APP_NAME || 'Laravel';

createInertiaApp({
    title: (title) => `${title} - ${appName}`,
    resolve: (name) => resolvePageComponent(
        `./Pages/${name}.vue`,
        import.meta.glob('./Pages/**/*.vue')
    ),
    setup({ el, App, props, plugin }) {
        return createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue)
            .mount(el);
    },
});

Paso 4: Creando la página de bienvenida mejorada

Ahora vamos a mejorar nuestra página de bienvenida usando los conceptos aprendidos:

vue
<template>
    <div>
        <!-- Título dinámico -->
        <Head>
            <title>Inicio - Mi Portafolio</title>
            <meta name="description" content="Bienvenido a mi portafolio profesional" />
        </Head>

        <!-- Barra de navegación -->
        <nav class="bg-gray-800 px-12 py-4 flex justify-between items-center">
            <ApplicationLogo class="w-12 h-12" />

            <div class="flex items-center space-x-4">
                <template v-if="user">
                    <Link :href="route('dashboard')" class="text-gray-300 hover:text-white">
                        Dashboard
                    </Link>
                </template>
                <template v-else>
                    <Link :href="route('login')" class="text-gray-300 hover:text-white">
                        Iniciar Sesión
                    </Link>
                    <Link :href="route('register')" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
                        Registrarse
                    </Link>
                </template>
            </div>
        </nav>

        <!-- Sección Hero -->
        <Section class="bg-gray-800 min-h-screen">
            <div class="flex items-center justify-center h-full">
                <div class="text-center text-white">
                    <h1 class="text-6xl font-bold mb-6">
                        Hola, soy {{ user?.name || 'Juan' }}
                    </h1>
                    <p class="text-2xl text-gray-300 mb-8">
                        Software Engineer & Full Stack Developer
                    </p>
                    <Link href="#projects" class="px-8 py-4 bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors">
                        Ver mi trabajo
                    </Link>
                </div>
            </div>
        </Section>

        <!-- Sección de Habilidades -->
        <Section background="gray-100" text="gray-800">
            <h2 class="text-4xl font-bold mb-8">Habilidades</h2>
            <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
                <div v-for="skill in skills" :key="skill.id"
                     class="p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
                    <div class="flex items-center justify-between">
                        <span class="font-medium">{{ skill.name }}</span>
                        <span class="text-sm text-gray-500">{{ skill.level }}%</span>
                    </div>
                    <div class="w-full bg-gray-200 rounded-full h-2 mt-2">
                        <div class="h-2 rounded-full transition-all duration-500"
                             :class="`bg-${skill.color}-500`"
                             :style="{ width: `${skill.level}%` }">
                        </div>
                    </div>
                </div>
            </div>
        </Section>

        <!-- Sección de Proyectos -->
        <Section background="gray-800" text="white">
            <h2 class="text-4xl font-bold mb-8">Proyectos Destacados</h2>
            <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
                <div v-for="project in projects" :key="project.id"
                     class="bg-white rounded-xl overflow-hidden shadow-lg hover:shadow-2xl transition-shadow">
                    <img :src="project.image_url" :alt="project.title" class="w-full h-48 object-cover">
                    <div class="p-6">
                        <h3 class="text-xl font-bold text-gray-800 mb-2">
                            {{ project.title }}
                        </h3>
                        <p class="text-gray-600 mb-4">
                            {{ project.description }}
                        </p>
                        <div class="flex flex-wrap gap-2 mb-4">
                            <span v-for="tech in project.technologies" :key="tech"
                                  class="px-3 py-1 bg-gray-200 rounded-full text-sm text-gray-700">
                                {{ tech }}
                            </span>
                        </div>
                        <Link :href="route('projects.show', project.slug)"
                              class="inline-block px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
                            Ver más
                        </Link>
                    </div>
                </div>
            </div>
        </Section>

        <!-- Footer -->
        <footer class="bg-gray-900 text-gray-400 px-12 py-8">
            <div class="flex justify-between items-center">
                <p>&copy; 2024 {{ user?.name || 'Juan' }}. Todos los derechos reservados.</p>
                <div class="flex space-x-6">
                    <Link href="https://github.com" class="hover:text-white transition-colors">
                        GitHub
                    </Link>
                    <Link href="https://twitter.com" class="hover:text-white transition-colors">
                        Twitter
                    </Link>
                    <Link href="https://linkedin.com" class="hover:text-white transition-colors">
                        LinkedIn
                    </Link>
                </div>
            </div>
        </footer>
    </div>
</template>

<script setup>
import { Head, Link, usePage } from '@inertiajs/vue3';
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
import Section from '@/Components/Section.vue';

// Datos recibidos del backend
const props = defineProps({
    user: Object,
    skills: Array,
    projects: Array,
    canLogin: Boolean,
    canRegister: Boolean,
});

// Obtener datos del usuario desde la página
const page = usePage();
const user = page.props.auth.user;
</script>

Paso 5: Creando el componente Section personalizado

Crea resources/js/Components/Section.vue:

vue
<template>
    <section class="px-12 py-16"
             :class="[
                 background ? `bg-${background}` : 'bg-white',
                 text ? `text-${text}` : 'text-gray-800',
                 minHeight ? `min-h-screen` : ''
             ]">
        <div class="max-w-7xl mx-auto">
            <slot />
        </div>
    </section>
</template>

<script setup>
defineProps({
    background: {
        type: String,
        default: 'white'
    },
    text: {
        type: String,
        default: 'gray-800'
    },
    minHeight: {
        type: Boolean,
        default: false
    }
});
</script>

Paso 6: Navegación con Inertia

6.1 Navegación programática

Usar router para navegación desde JavaScript:

vue
<script setup>
import { router } from '@inertiajs/vue3';

const goToProjects = () => {
    router.visit('/projects', {
        method: 'get',
        data: { filter: 'featured' },
        preserveScroll: true,
        onSuccess: () => console.log('Navegación exitosa')
    });
};

const submitForm = () => {
    router.post('/projects', {
        title: 'Nuevo Proyecto',
        description: 'Descripción del proyecto'
    }, {
        onSuccess: () => {
            console.log('Proyecto creado');
        },
        onError: (errors) => {
            console.error(errors);
        }
    });
};
</script>

6.2 Compartir datos globalmente

En App\Http\Middleware\HandleInertiaRequests.php:

php
public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'appName' => config('app.name'),
        'auth' => [
            'user' => $request->user(),
        ],
        'flash' => [
            'success' => fn () => $request->session()->get('success'),
            'error' => fn () => $request->session()->get('error'),
        ],
    ]);
}

Ahora puedes acceder a estos datos desde cualquier página:

vue
<script setup>
import { usePage } from '@inertiajs/vue3';

const page = usePage();
const appName = page.props.appName;
const user = page.props.auth.user;
const flash = page.props.flash;
</script>

Paso 7: Manejo de formularios con Inertia

Inertia facilita el manejo de formularios con useForm:

vue
<template>
    <form @submit.prevent="submit">
        <div class="space-y-4">
            <div>
                <Label for="title">Título</Label>
                <Input id="title" v-model="form.title" type="text" class="w-full" />
                <InputError :message="form.errors.title" />
            </div>

            <div>
                <Label for="description">Descripción</Label>
                <Textarea id="description" v-model="form.description" class="w-full" />
                <InputError :message="form.errors.description" />
            </div>

            <Button type="submit" :disabled="form.processing">
                <span v-if="form.processing">Guardando...</span>
                <span v-else>Guardar Proyecto</span>
            </Button>
        </div>
    </form>
</template>

<script setup>
import { useForm } from '@inertiajs/vue3';
import Button from '@/Components/Button.vue';
import Input from '@/Components/Input.vue';
import InputError from '@/Components/InputError.vue';
import Label from '@/Components/Label.vue';
import Textarea from '@/Components/Textarea.vue';

const form = useForm({
    title: '',
    description: '',
});

const submit = () => {
    form.post('/projects', {
        onSuccess: () => {
            form.reset();
            // Mostrar mensaje de éxito
        },
        preserveScroll: true,
    });
};
</script>

Paso 8: Mejores prácticas con Inertia y Vue

8.1 Organización de componentes

text
resources/js/
├── Components/          # Componentes reutilizables
│   ├── Button.vue
│   ├── Card.vue
│   ├── Input.vue
│   └── Section.vue
├── Layouts/            # Layouts de la aplicación
│   └── AppLayout.vue
└── Pages/              # Páginas (vistas)
    ├── Welcome.vue
    ├── Dashboard.vue
    ├── Projects/
    │   ├── Index.vue
    │   └── Show.vue
    └── Skills/
        └── Index.vue

8.2 Carga perezosa de páginas

javascript
// En app.js
resolve: (name) => {
    return import(`./Pages/${name}.vue`).then(module => module.default);
}

8.3 Manejo de errores

vue
<template>
    <div v-if="page.props.errors" class="bg-red-50 p-4 rounded-md">
        <ul class="text-red-700">
            <li v-for="(error, key) in page.props.errors" :key="key">
                {{ error }}
            </li>
        </ul>
    </div>
</template>

<script setup>
import { usePage } from '@inertiajs/vue3';

const page = usePage();
</script>

Solución de problemas comunes

Error: "Componente no encontrado"

Asegúrate de importar y registrar el componente:

javascript
import { Head, Link } from '@inertiajs/vue3';
// Registrar en components

Error: "No se puede resolver la ruta"

Verifica que Ziggy esté correctamente configurado:

javascript
// En app.js
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
app.use(ZiggyVue);

Los cambios no se reflejan

Asegúrate de tener npm run watch ejecutándose en la terminal.

Error 419 CSRF

Las peticiones POST desde Inertia manejan automáticamente el token CSRF.


Resumen

✅ Comprendimos qué es Inertia.js y cómo funciona con Vue.js
✅ Aprendimos a usar los componentes <Head> y <Link>
✅ Creamos una página de bienvenida con datos reales
✅ Construimos un componente Section reutilizable
✅ Aprendimos a navegar programáticamente con Inertia
✅ Implementamos manejo de formularios con useForm
✅ Conocimos las mejores prácticas de organización

¡Excelente trabajo! Ahora tienes una base sólida para construir aplicaciones SPA con Laravel, Inertia y Vue.js. En las próximas lecciones profundizaremos en funcionalidades más avanzadas y comenzaremos a construir las páginas específicas de nuestro portafolio.


Recursos adicionales


¿Tienes dudas sobre Inertia o Vue? La documentación oficial y los ejemplos prácticos del curso te ayudarán a dominar estas herramientas

Comentarios

Entradas más populares de este blog

Tareas con Laravel + Inertia + Jetstream

HACER CAMBIOS-AL EJEMPLO POR DEFECTO

Laravel + Inertia - ejemplo