
VueJS doesn't have to be a Single-Page Application
What is VueJS?
VueJS is a progressive JavaScript framework that is incredibly adaptable. It is often brought up along with other popular JavaScript frameworks and libraries such as ReactJS and Angular. It was made by Evan You (also the founder of Vite which is a widely used frontend build tool) and has a large ecosystem of libraries and frameworks that are widely used throughout VueJS applications and pages. VueJS is often compared against ReactJS as they look and function somewhat similarly with a few differences.
The Goal of this Article
Often times Vue is known to be a Single-Page Application, and while it definitely can and it does a great job doing so, I would like to show an alternative way of utilizing Vue. Instead of using full Single-Page Application architecture, Vue can be used to easily plug and play into existing pages and applications with relative ease without the requirement of having a full SPA with routing, state management and all of the other complexities that come with a Single-Page Applications.
This article will not be an in depth guide on how to use Vue (though we will touch a few introductory level concepts). In this article, I will be using raw HTML pages as well as a .NET application with Razor pages to show how we can implement Vue into certain pages, however these same principles and ideas can be utilized across various scenarios. For the examples that will be shown in this post we will be using the modern Vue3 version, however the older Vue2 version may also be used.
Why Would I Not Just Use a Single-Page Application?
Vue does a great job of being a Single-Page Application but there are a few reasons why you may want to go this route rather than building an entire SPA:
- Adoption With Legacy Codebases
- Often times as developers we have a pre-existing codebase we need to work with. Let's say that we have a 10 year old .NET MVC Application and we want to add modern Vue functionality to it. To do this would potentially cost months of development time rewriting the entire codebase.
- Often times as developers we have a pre-existing codebase we need to work with. Let's say that we have a 10 year old .NET MVC Application and we want to add modern Vue functionality to it. To do this would potentially cost months of development time rewriting the entire codebase.
- Reduced Complexity
- SPAs introduce overhead that may not be justified for your specific use case:
- SPA Routing - Separate frontend routing
- Build Tooling - Managing the full frontend ecosystem
- API Design - Full API interface is needed for all data
- State Management - For complex state management, some form of state management tooling is needed to track state across various pages (VueX, Pinia)
- SPA Routing - Separate frontend routing
- SPAs introduce overhead that may not be justified for your specific use case:
- Simplifying Deployments
- Keeping Vue as a non-SPA integration means there are no changes needed for deployment pipelines
- Keeping Vue as a non-SPA integration means there are no changes needed for deployment pipelines
- Gradual Adoption Process
- Adding Vue to individual pages can allow for a team to get familiar with Vue and slowly start sprinkling in pieces as quickly or as slowly as they want
The Basics with plain HTML
Let's start with a very basic HTML page with a simple table on it:
1<body>
2 <div class="container" id="tableapp">
3 <h1>User Data Table</h1>
4 <table>
5 <thead>
6 <tr>
7 <th>ID</th>
8 <th>Name</th>
9 <th>Email</th>
10 <th>Role</th>
11 <th>Status</th>
12 </tr>
13 </thead>
14 <tbody>
15 <tr>
16 <td>1</td>
17 <td>John Doe</td>
18 <td>john.doe@example.com</td>
19 <td>Admin</td>
20 <td>Active</td>
21 </tr>
22 <tr>
23 <td>2</td>
24 <td>Jane Smith</td>
25 <td>jane.smith@example.com</td>
26 <td>User</td>
27 <td>Active</td>
28 </tr>
29 <tr>
30 <td>3</td>
31 <td>Bob Johnson</td>
32 <td>bob.johnson@example.com</td>
33 <td>User</td>
34 <td>Inactive</td>
35 </tr>
36 <tr>
37 <td>4</td>
38 <td>Alice Williams</td>
39 <td>alice.williams@example.com</td>
40 <td>Manager</td>
41 <td>Active</td>
42 </tr>
43 </tbody>
44 </table>
45 </div>
46</body>To add VueJS to this page, it's pretty simple. We need to import Vue and then mount it to an element.
- Add the script to the file
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>- Create the Vue instance and mount it to an element (ideally a root level element where you want the Vue functionality to function)
1<script>
2 const { createApp, ref } = Vue
3 createApp({
4 setup() {
5 const message = ref('Hello vue!')
6 return {
7 message
8 }
9 }
10 }).mount('#tableapp')
11</script>And just like that, we now have a Vue instance on the page.
- Then we can use that message ref and display it on our page
1<body>
2 <div class="container" id="tableapp">
3 <!-- Add message here -->
4 <div>{{ message }}</div>
5 <h1>User Data Table</h1>
6 <table>VueJS proves that modern doesn’t have to mean a full rebuild. You can start small, enhance what you already have, and evolve your app at your own pace—without the overhead of a full SPA. Whether it’s a simple component or a richer interactive experience, Vue gives you the flexibility to modernize on your terms.
Let’s transform this page into a Vue driven page by setting the data in Vue and then adding a search feature.
4. Setting up the table data
1// Within the setup() function
2// In practice this would likely come from some API GET request, or some application data (more on this in our .NET example)
3
4const users = ref([
5 {
6 id: 1,
7 name: 'John Doe',
8 email: 'john.doe@example.com',
9 role: 'Admin', status: 'Active'
10 },
11 {
12 id: 2,
13 name: 'Jane Smith',
14 email: 'jane.smith@example.com',
15 role: 'User',
16 status: 'Active'
17 },
18 {
19 id: 3,
20 name: 'Bob Johnson',
21 email: 'bob.johnson@example.com',
22 role: 'User',
23 status: 'Inactive'
24 },
25 {
26 id: 4,
27 name: 'Alice Williams',
28 email: 'alice.williams@example.com',
29 role: 'Manager',
30 status: 'Active'
31 }
32])5. Using Vue Directives to display the user data in the body
1<body>
2 <div class="container" id="tableapp">
3 <h1>User Data Table</h1>
4 <table>
5 <thead>
6 <tr>
7 <th>ID</th>
8 <th>Name</th>
9 <th>Email</th>
10 <th>Role</th>
11 <th>Status</th>
12 </tr>
13 </thead>
14 <tbody>
15 <!-- Iterate our user data and bind a unique key -->
16 <tr v-for="user in users" :key="user.id">
17 <td>{{ user.id }}</td>
18 <td>{{ user.name }}</td>
19 <td>{{ user.email }}</td>
20 <td>{{ user.role }}</td>
21 <td>{{ user.status }}</td>
22 </tr>
23 </tbody>
24 </table>
25 </div>
26</body>A note on VueJS syntax
While this won't be a detailed guide on VueJS, I do want to quickly go over what we have so far and some Vue basics.
- Template syntax
- The way to display Vue data inside of a template (in our tableapp) is the `{{ }}` syntax where the content inside of the brackets is treated as vue javascript. This could contain a vue defined variable such as our `message` ref, a function, or plain javascript operations like `1 + 1` and the result would display `2`
- Vue Directives
- Vue operates on a system of directives which are very helpful with interacting with the DOM
- Some common directives:
- `v-for` iterates over a given variable (as shown in our user iteration)
- `v-if` conditionally renders elements depending on the content within the directive
- `v-model` two way binding which updates data value with input
- for more details on directives visit the vue directive documentation
- `createApp` function
- creates a new vue application instance
- `ref`
- recommended way to make a value of a variable "reactive" meaning it will play well within VueJS and it can track changes and reactivity of the value
- lifecycle hooks
- lifecycle hooks can be used to access vue at various points such as when the instance is mounted on the page. For more information on lifecycle hooks visit the Vue documentation
- computed properties
- Used to return a calculated value based on reactive dependencies (this is why we use refs!) - this will make more sense once we use a computed property for our search functionality
With that out of the way, lets use vue to add a searchbar to our table
6. Add a ref variable for to store our search input
const searchQuery = ref('')7. Add our searchbox and bind the input to our searchQuery ref
1<div class="search-box">
2 <input
3 type="text"
4 v-model="searchQuery"
5 placeholder="Search by name, email, role, or status..."
6>
7</div>8. Add a computed property to return a list of filtered users (by our search input)
1const filteredUsers = computed(() => {
2 // If our search input is empty return all users (no search)
3 if (!searchQuery.value) {
4 return users.value
5 }
6
7 // Set the query to all lowercase to filter regardless of upper/lower
8 const query = searchQuery.value.toLowerCase()
9
10 // filter users by name, email, role, and status using our case insensitive query
11 return users.value.filter(user => {
12 return (
13 user.name.toLowerCase().includes(query) ||
14 user.email.toLowerCase().includes(query) ||
15 user.role.toLowerCase().includes(query) ||
16 user.status.toLowerCase().includes(query)
17 )
18 })
19 })9. Import computed
1// add computed here
2const { createApp, ref, computed } = Vue10. Update return
1return {
2 message,
3 searchQuery,
4 users,
5 filteredUsers
6 }11. Change our table v-for to loop over filteredUsers instead of users
<tr v-for="user in filteredUsers" :key="user.id">One last finishing touch before we get into .NET and razor pages with Vue, let's add an external library to add some flair to our table. Let’s use Vuetify (My personal favorite VueJS UI library) to add chips to our active and inactive values on our table.
12. Import the external library
<script src="https://cdn.jsdelivr.net/npm/vuetify@3.4.9/dist/vuetify.min.js"></script>13. Define Vuetify inside of our Vue instance
1const { createVuetify } = Vuetifyconst
2vuetify = createVuetify()14. Add use to our Vue instantiation
1// add the .use(vuetify) here
2}).use(vuetify).mount('#tableapp')15. Wrap vue instance content in boilerplate (under the `<div id="tableapp">`)
<!-- v-app is required by vuetify -->
<v-app>
<!-- v-main isn't required but it is recommended as it designates the main area of the content and will handle UI better when used in conjunction with other vuetify components like v-footer, etc -->
<v-main>16. Add our v-chip component from vuefity
1<td>{{ user.email }}</td>
2 <td>{{ user.role }}</td>
3 <td>
4 <!-- this is our vuetify v-chip component -->
5 <v-chip
6 :color="user.status === 'Active' ? 'green' : 'red'"
7 size="small"
8 text-color="white"
9>
10 {{ user.status }}
11 </v-chip>
12 </td>Now we will go into a .NET application example and add Vue to a page and see how it interacts with page data and how we can set up reusable components to be used throughout our application.
First, let's add the Vue library import similar to how we added it on the raw HTML page. Typically in .NET applications, there is a layout file, so we will add it there so it is globally available on all pages that use our layout. I will also be using Bootstrap for styling, so add in bootstrap to the layout file as well.
1<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
2<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>Let’s set up a page with some mock data being returned so we can use that in our Vue instance
1// index.cshtml.cs file
2
3namespace VueWithDotNet.Pages
4{
5 public class IndexModel : PageModel
6 {
7 private readonly ILogger<IndexModel> _logger;
8
9 // Mock data
10 private static readonly List<Product> _products = new()
11 {
12 new Product { Id = 1, Name = "Laptop", Price = 999.99m, Category = "Electronics", InStock = true },
13 new Product { Id = 2, Name = "Mouse", Price = 29.99m, Category = "Electronics", InStock = true },
14 new Product { Id = 3, Name = "Keyboard", Price = 79.99m, Category = "Electronics", InStock = false },
15 new Product { Id = 4, Name = "Monitor", Price = 299.99m, Category = "Electronics", InStock = true },
16 new Product { Id = 5, Name = "Desk Chair", Price = 199.99m, Category = "Furniture", InStock = true }
17 };
18
19 public IndexModel(ILogger<IndexModel> logger)
20 {
21 _logger = logger;
22 }
23
24 // Properties accessible in the Razor view
25 public List<Product> Products { get; set; } = new();
26 public int TotalProducts { get; set; }
27
28 // Handler for initial page load
29 public void OnGet()
30 {
31 Products = _products;
32 TotalProducts = _products.Count;
33 }
34 }
35}Let's add a Product class to our `Models/` folder
1namespace VueWithDotNet.Models
2{
3 public class Product
4 {
5 public int Id { get; set; }
6 public string Name { get; set; } = string.Empty;
7 public decimal Price { get; set; }
8 public string Category { get; set; } = string.Empty;
9 public bool InStock { get; set; }
10 }
11}Now for our index.cshtml file, lets setup a basic page that uses our model data
1@page
2@model IndexModel
3@{
4 ViewData["Title"] = "Home page";
5}
6
7<div class="container">
8 <h1 class="display-4">Product Catalog</h1>
9
10 <p class="lead">Total Products: @Model.TotalProducts</p>
11
12 <div class="row">
13 <div class="col-md-12">
14 <h2>Product List</h2>
15 <table class="table table-striped">
16 <thead>
17 <tr>
18 <th>ID</th>
19 <th>Name</th>
20 <th>Price</th>
21 <th>Category</th>
22 <th>In Stock</th>
23 </tr>
24 </thead>
25 <tbody>
26 @foreach (var product in Model.Products)
27 {
28 <tr>
29 <td>@product.Id</td>
30 <td>@product.Name</td>
31 <td>$@product.Price</td>
32 <td>@product.Category</td>
33 <td>
34 @if (product.InStock)
35 {
36 <span class="badge bg-success">Yes</span>
37 }
38 else
39 {
40 <span class="badge bg-danger">No</span>
41 }
42 </td>
43 </tr>
44 }
45 </tbody>
46 </table>
47 </div>
48 </div>
49</div>
50
51@section Scripts {
52 <script type="application/json" id="productsData">
53 @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Products))
54 </script>
55}Now let’s instantiate our Vue instance on our index.cshtml page by adding the following underneath our script for Model.Products. So our @section Scripts should be the following
1@section Scripts {
2 <script type="application/json" id="productsData">
3 @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Products))
4 </script>
5
6 <script>
7 const { createApp, ref } = Vue;
8
9 // Counter app
10 createApp({
11 setup() {
12
13 return {
14
15 };
16 }
17 }).mount('#vueApp');
18 </script>
19}Now let’s add some test functionality using Vue by adding a simple counter with Vue functions. We will add an increment, decrement, and reset button. Later we will create a reusable custom button component that we can reuse on multiple pages.
1. Add our Vue functions and variables and return them
1const count = ref(0);
2
3// Methods
4const increment = () => {
5 count.value++;
6};
7
8const decrement = () => {
9 count.value--;
10};
11
12const reset = () => {
13 count.value = 0;
14};
15
16return {
17 count,
18 increment,
19 decrement,
20 reset
21};2. Add the element root id so that Vue can mount to the page
- You don't have to add the ID to the pages root element like I did here, you can have it containerized to a specific portion of the page if preferred
- By mounting it to the root element like I did, the entire page content will be accessible by Vue
<div class="container" id="vueApp">3. Add our counting functionality
1<div class="card p-4">
2 <p class="lead">Count: <strong>{{ count }}</strong></p>
3
4 <div class="btn-group" role="group">
5 <button @@click="increment" class="btn btn-primary">
6 Increment
7 </button>
8 <button @@click="decrement" class="btn btn-secondary">
9 Decrement
10 </button>
11 <button @@click="reset" class="btn btn-warning">
12 Reset
13 </button>
14 </div>
15</div>An important note on using Vue event bindings in razor pages (like our click even bindings for each button):
- When using Vue inside of razor pages, we must use double `@@` symbols. This is because a single `@` is reserved by Razor and if we were to use a single `@` razor would try and interpret this as C#
- Usually when using Vue the event bindings (like click) they use a single `@` but for razor pages, we cannot do this.
- Other examples of Vue event are `@change`, `@submit`, `@input`, etc. For more information on Vue events visit the Vue documentation on event handling.
Now let’s take our model data and incorporate it with our Vue instance.
1. Load the model product data into Vue - this should go above or below the `const { createApp, ref } = Vue` line. We can also remove the existing script tag
1// remove this existing tag
2<script type="application/json" id="productsData">
3 @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Products))
4</script>1// Add thisconst products
2Data = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Products));2. In our vue instance setup() function, let’s add the products ref and return it
1const products = ref(productsData);
2
3...
4
5return {
6 count,
7 // Add products to the return
8 products,
9 increment,
10 decrement,
11 reset
12
13};3. Now similar to our HTML example, we can use that products data inside of our page using Vue
1<div class="col-md-12">
2 <h2>Product List</h2>
3
4 <table class="table table-striped">
5 <thead>
6 <tr>
7 <th>ID</th>
8 <th>Name</th>
9 <th>Price</th>
10 <th>Category</th>
11 <th>In Stock</th>
12 </tr>
13 </thead>
14 <tbody>
15 <!-- iterate our products list and bind the unique ID -->
16 <tr v-for="product in products" :key="product.Id">
17 <td>{{ product.Id }}</td>
18 <td>{{ product.Name }}</td>
19 <td>${{ product.Price }}</td>
20 <td>{{ product.Category }}</td>
21 <td>
22 <!-- Use vue templating to display yes or no depending on the value of InStock -->
23 {{ product.InStock ? 'Yes' : 'No' }}
24 </td>
25 </tr>
26 </tbody>
27 </table>
28</div>And just like that, we took our model page and placed it inside of our Vue instance and rendered it on the page using Vue.
Now to finish this off, let's build a reusable button component outside of this page, that we can import and use on other pages. We will use the button component on our simple counter example.
1. Create a `Component/` folder to store our reusable components (I placed this inside of the Pages folder)
2. Add the AppButton.cshtml component
- While we are creating a .cshtml file so that razor can render it via @Html.PartialAsync, the contents inside of this file are primarily vue component code
1@* AppButton Component - Reusable button with type/color variants *@
2
3<script type="text/x-template" id="app-button-template">45678910</script>
11
12<script type="text/javascript">
13 const AppButtonComponent = {
14 name: 'AppButton',
15 template: '#app-button-template',
16 props: {
17 type: {
18 type: String,
19 default: 'primary',
20 validator: (value) => {
21 return ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'].includes(value);
22 }
23 },
24 size: {
25 type: String,
26 default: 'md',
27 validator: (value) => {
28 return ['sm', 'md', 'lg'].includes(value);
29 }
30 },
31 disabled: {
32 type: Boolean,
33 default: false
34 }
35 },
36
37 computed: {
38 buttonClasses() {
39 const classes = ['btn', `btn-${this.type}`];
40
41 if (this.size !== 'md') {
42 classes.push(`btn-${this.size}`);
43 }
44
45 return classes.join(' ');
46 }
47 }
48 };
49</script>A few important notes on the component structure:
- `<script type="text/x-template" id="app-button-template">` this tag is important to define as text/x-template and the id must match the template name found in the AppButtonComponent.Template definition
- this .cshtml is to be imported into a Vue instance (like our index page) so it will be treated as vue once imported and used inside of that instance
- Vue naming is interesting in that the "Name" being `AppButton` will be converted to `app-button` when used inside of the DOM. Props are the same way, where if you had a prop defined as `ButtonLabel`, when you use that prop on the component usage it should be `:button-label="labelName"`
- for more information on Vue "props" visit the props documentation
- The `<slot>` is used for content that you want to place in the component that will render in the slot area of the component. So if for example when we place the `app-button` on the index page we can add text to it by the following
1<app-button type="primary">
2 <!-- Increment text will be rendered inside of <slot></slot> -->
3 Increment
4</app-button>Now to finish this off, let's add our new reusable component to our index page
1. Add the import for the component (under the `ViewData["Title"]` tag)
1@* Import Vue Components *@
2@await Html.PartialAsync("Components/AppButton")2. Register the component in Vue
1// Register components before mounting
2// Insert this line before mounting the app with app.mount('#vueApp')
3app.component('AppButton', AppButtonComponent);3. Add our button to our simple counter
1<div class="btn-group" role="group">
2 <app-button type="primary" @@click="increment">
3 Increment
4 </app-button>
5 <app-button type="secondary" @@click="decrement">
6 Decrement
7 </app-button>
8 <app-button type="warning" @@click="reset">
9 Reset
10 </app-button>
11</div>Key Takeaway
With the approach that we have taken we have made a .NET application that:
- ✅ Adds a Vue interface to the index page which allows for Vue interactivity (counter, search filtering)
- ✅ Utilizes server-side data with no additional API needed to fetch data (Product data)
- ✅ Created a reusable component that we can use across multiple page to prevent duplicate code (App Button component)
- ✅ No additional build pipeline needed
- ✅ Works with existing deployment pipelines
- ✅ Utilized third party UI library to add additional functionality (Vuetify)
Bonus Vue tools
Bonus Libraries and Frameworks to consider for Vue (that I have used on MPAs!):
- Vuetify - My favorite Vue UI library
- VeeValidate - My favorite Vue frontend validation library
- Vue Devtools - A very helpful web extension tool when debugging vue applications and instances
Frequently Asked Questions
Latest Posts
We’ve helped our partners to digitally transform their organizations by putting people first at every turn.

.avif)
























