How to modernise legacy list views progressively by mounting Vue 3 dynamic components inside Laravel Blade templates without a full SPA rewrite.
When managing large enterprise applications (like ERP or inventory platforms), list views for clients, stocks, warehouse movements, and suppliers are the core of daily operations. Over time, traditional jQuery Datatables or fully server-rendered lists become slow, unresponsive, and hard to maintain.
However, a full Single Page Application (SPA) rewrite is risky and expensive. Let's look at how to implement a hybrid architecture by progressively embedding reactive Vue 3 Datatables inside existing Laravel Blade templates.
Instead of converting the entire application to Vue, we keep the main layouts, sidebar, and routing in Laravel Blade. We then mount Vue 3 components on specific HTML elements on demand:
<!-- Laravel Blade Template: inventories/index.blade.php -->
@extends('layouts.app')
@section('content')
<div class="container mx-auto py-6">
<h1 class="text-2xl font-bold mb-6">Inventory Management</h1>
<!-- Mounting element for Vue 3 -->
<div id="vue-stock-listing" data-api-url="{{ route('api.inventory.index') }}"></div>
</div>
@endsectionIf your Laravel application uses Vite, configure it to load the Vue 3 plugin and build single-file components (.vue).
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});In your main entry script, check if the specific container exists before mounting the Vue app. This prevents runtime errors on other pages that do not have Vue components.
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// resources/js/app.js
import { createApp } from 'vue';
import StockListing from './components/StockListing.vue';
const targetElement = document.getElementById('vue-stock-listing');
if (targetElement) {
const apiUrl = targetElement.getAttribute('data-api-url');
const app = createApp(StockListing, {
apiUrl: apiUrl
});
app.mount(targetElement);
}Now, we build a high-performance listing component that queries our Laravel controller endpoint, handles filters, and manages pagination state reactively.
<!-- resources/js/components/StockListing.vue -->
<template>
<div class="bg-white border rounded-xl p-6 shadow-sm">
<div class="flex justify-between items-center mb-6">
<input
v-model="search"
@input="debouncedFetch"
placeholder="Search stock..."
class="border rounded-lg px-4 py-2 text-sm w-64"
/>
</div>
<table class="w-full text-left border-collapse">
<thead>
<tr class="border-b bg-gray-50 text-xs font-semibold text-gray-500 uppercase">
<th class="p-4">SKU</th>
<th class="p-4">Name</th>
<th class="p-4">Quantity</th>
<th class="p-4">Location</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id" class="border-b hover:bg-gray-50">
<td class="p-4 font-mono">{{ item.sku }}</td>
<td class="p-4 font-semibold">{{ item.name }}</td>
<td class="p-4">{{ item.qty }} units</td>
<td class="p-4">{{ item.location }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
const props = defineProps({
apiUrl: String
});
const items = ref([]);
const search = ref('');
const fetchItems = async () => {
try {
const response = await axios.get(props.apiUrl, {
params: { q: search.value }
});
items.value = response.data.data;
} catch (error) {
console.error("Failed to load inventory data:", error);
}
};
onMounted(() => {
fetchItems();
});
</script>This progressive approach lets you transition your legacy ERP views to a modern user interface block-by-block without breaking the overall system flow.