Vue 3

Vue is Too Easy

Front end development is a very polarized industry. We argue about programming languages, frameworks, and even the best indentation style…

Fotis Adamakis
Fotis Adamakis
Senior Software Engineer / Technical Writer
9 min read
February 12, 2025

Vue is Too Easy

Front end development is a very polarized industry. We argue about programming languages, frameworks, and even the best indentation style. But one thing that everyone agrees on is that Vue has the easiest learning curve.

And that’s by design! Vue was built to stay out of your way during development. But don’t get fooled, It’s extremely powerful, scalable, and capable of supporting any architecture.

Let’s investigate all the parts that make this possible.

1. Component Declaration

In Vue, components are written inside .vue files, known as Single File Components (SFCs).

Each component has three distinct sections:

  • <template> Defines the component’s UI structure.
  • <script setup> Handles logic, state, and imports.
  • <style scoped> Contains styles specific to this component.
<template>
  <!-- Template: The UI structure -->
  <h1>Hello, Vue World!</h1>
</template>

<script setup>
// Script: Logic like variables and methods
</script>

<style scoped>
/\* Styles: Component-specific CSS \*/
</style>

All three sections are optional. A component can be just a template, just logic, or even just styles if needed.

Now, let’s explore each section in more detail.

2. Template

The template section defines the component UI. It’s very close to HTML but includes additional directives that make it more powerful.

Basic example:

<template>
  <h1>{{ message }}</h1>
  <button @click="increment">Click me</button>
  <p>Count: {{ count }}</p>
</template>
  • {{ message }} Displays a reactive variable inside the template. (interpolation)
  • @click="increment" Binds an event listener to the button.

Conditional Rendering
The paragraph will only appear when isVisible is true.

<p v-if="isVisible">This text is conditionally rendered.</p>

Rendering Lists
Loops through an array and dynamically renders each item.

<ul>
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>

Binding Attributes
We can dynamically assign values to attributes using v-bind (or its shorthand :).

<img :src="imageUrl" alt="Vue logo" />

The template system is declarative and reactive, and the UI will update automatically whenever the component data changes.

3. Styles

Styling in Vue is straightforward. You can write regular CSS inside the <style> section, just like in a standard stylesheet. Vue makes it easy to scope styles, use preprocessors, and rely on the bundler to optimize styles for production.

Scoped Styles
By default, styles in a Vue component apply globally. However, adding the scoped attribute will generate unique class names to prevent styles from leaking into other components.

<style scoped>
  h1 {
    color: #42b983;
  }
</style>

Preprocessors
Vue supports CSS preprocessors like SCSS, Less, and Stylus out of the box. We just need to specify the preprocessor inside the <style> tag:

<style scoped lang="scss">
  $primary-color: #42b983;

  h1 {
    color: $primary-color;
    font-weight: bold;
  }
</style>

This makes it easy to organize styles, reuse variables, and write cleaner CSS.

The bundler (Vite or Webpack) will automatically extract, minify, and optimize the styles, improving loading time and performance in production.

4. Script Setup

The last part of a Vue component is the <script setup> section, where we define logic, state, and imports.

<template>
  <h1>{{ message }}</h1>
  <button @click="increment">Click me</button>
  <Counter :count="count" />
</template>

<script setup>
import Counter from "./Counter.vue";

const message = "Hello, Vue!";
const count = 0;

function increment() {
  // TODO
}
</script>

Every variable and function declared inside <script setup> is automatically available to the template. No need to return anything, Vue will take care of it.

There’s only one problem with the code above: variables are not reactive! Changes to count won’t trigger a UI update.

5. Reactivity

To declare reactive variables we need to use the built-in ref() and reactive() helpers.

<script setup>
import { ref } from "vue";

const count = ref(0);

const increment = () => count.value++;
</script>
  • ref(0) creates a reactive variable.
  • .value is required to access and modify its value.

Now, every time count changes, Vue will automatically update the template.

For objects and arrays, we use reactive() instead.

<script setup>
import { reactive } from "vue";

const user = reactive({ name: "Alice", age: 25 });

user.age++;
</script>

Notice that with reactive the .value is not needed.

[Vue 3] Why both Ref and Reactive are needed

6. State Management

When multiple components need to share data, we need to be more mindful about state management patterns. Vue offers different ways to handle state, depending on the complexity of the application.

Lifting State Up
For simple cases, state can be managed at the parent level and passed down via props, while updates are sent back using emits.

<!-- Parent.vue -->
<template>
  <Counter :count="count" @increment="increment" />
</template>

<script setup>
import { ref } from "vue";
import Counter from "./Counter.vue";

const count = ref(0);

function increment() {
  count.value++;
}
</script>
<!-- Counter.vue -->
<template>
  <button @click="emit('increment')">Click me</button>
  <p>Count: {{ count }}</p>
</template>

<script setup>
const props = defineProps<{ count: number }>();
const emit = defineEmits<["increment"]>();
</script>
  • Props: parent pass data to the child.
  • Emits: child sends updates back to the parent.

This works well for small-scale applications but can become hard to manage when multiple components need access to the same state.

Pinia
For complex applications, [**Pinia**](https://pinia.vuejs.org/) is the recommended state management library. It provides a global store, integrates well with Vue DevTools, and has excellent TypeScript support.

Defining a Store

// stores/counter.ts
import { defineStore } from "pinia";
import { ref } from "vue";

export const useCounter = defineStore("counter", () => {
  const count = ref(0);
  const increment = () => count.value++;

  return { count, increment };
});

Using the Store in a Component

<script setup>
import { useCounter } from "@/stores/counter";

const counter = useCounter();
</script>

<template>
  <button @click="counter.increment">Click me</button>
  <p>Count: {{ counter.count }}</p>
</template>

Pinia automatically tracks changes and updates the UI and integrates well with vue devtools.

Composables

Lastly, there’s a third approach that is gaining popularity which is [**using composables for state management**](/composables-as-state-management-in-vue3-ad59837cad48). This method sits between the previous two, offering a lightweight approach without an external library like Pinia.

// composables/useCounter.ts
import { ref } from "vue";

const count = ref(0);

export function useCounter() {
  const increment = () => count.value++;

  return { count, increment };
}
<script setup>
import { useCounter } from "@/composables/useCounter";

const { count, increment } = useCounter();
</script>

<template>
  <button @click="increment">Click me</button>
  <p>Count: {{ count }}</p>
</template>

7. Routing

Let’s have a look at routing next.

While having just one popular plugin might seem limiting, [Vue Router](https://router.vuejs.org/) has everything you need.

Route declaration can be written in a clear and structured way, using a simple object-based configuration to define paths and their corresponding components.

import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import About from "@/views/About.vue";

const routes = [
  { path: "/", component: Home },
  { path: "/about", component: About },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Once the router is in place, it can be used inside components to navigate between pages.

<script setup>
import { RouterView, RouterLink } from "vue-router";
</script>

<template>
  <nav>
    <RouterLink to="/">Home</RouterLink>
    <RouterLink to="/about">About</RouterLink>
  </nav>
  <RouterView />
</template>
  • RouterLink is used for navigation, replacing traditional <a> tags.
  • RouterView dynamically loads the component that matches the current route.

Vue Router also supports dynamic parameters, allowing flexible route structures:

const routes = [{ path: "/user/:id", component: UserProfile }];

Inside the component, we can access the parameter using the useRoute helper.

<script setup>
import { useRoute } from "vue-router";

const route = useRoute();
const userId = route.params.id;
</script>

<template>
  <h1>User ID: {{ userId }}</h1>
</template>

The [**official Vue Router documentation**](https://router.vuejs.org/) covers more advanced topics, including navigation guards, lazy loading, and nested routes.

8. Async Components

Vue makes it easy to optimize the production bundle by combining lazy loading and tree shaking.

Instead of importing a component normally:

<script setup>
import ModalComponent from "@/components/ModalComponent.vue";
</script>

We can use Vue’s dynamic import to load it lazily:

<script setup>
import { defineAsyncComponent } from "vue";

const ModalComponent = defineAsyncComponent(
  () => import("@/components/ModalComponent.vue"),
);
</script>

This means the component is only fetched when it is first used, keeping the initial load fast.

Lazy loading is also supported at a route level:

const routes = [
  { path: "/", component: () => import("@/views/Home.vue") },
  { path: "/about", component: () => import("@/views/About.vue") },
];

Different pages are loaded only when a user navigates to them, preventing unnecessary code from being downloaded upfront.

9. Slots

With slots, we can define flexible placeholder areas inside a component where the parent can insert custom content.

Basic example using a default slot:

<!-- Card.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>

<style scoped>
.card {
  padding: 16px;
  border: 1px solid #ddd;
  border-radius: 8px;
}
</style>

Now, when using the Card component, we can provide any content inside it:

<template>
  <Card>
    <h2>Title</h2>
    <p>This is a slot example.</p>
  </Card>
</template>

<script setup>
import Card from "@/components/Card.vue";
</script>

Vue will replace <slot /> with the content passed by the parent.

Named Slots
A component can have multiple slots, each with a specific name:

<!-- Card.vue -->
<template>
  <div class="card">
    <header>
      <slot name="header" />
    </header>
    <main>
      <slot />
      <!-- Default slot -->
    </main>
    <footer>
      <slot name="footer" />
    </footer>
  </div>
</template>

Now, the parent can provide content for each slot separately:

<template>
  <Card>
    <template #header>
      <h2>Card Header</h2>
    </template>
    <p>Main content goes here.</p>
    <template #footer>
      <button>OK</button>
    </template>
  </Card>
</template>

Lastly, we can pass data back to the parent for more dynamic rendering using scoped slots. Learn more about them in the [**official Vue docs**](https://vuejs.org/guide/components/slots.html#scoped-slots).

10. Animations

Vue provides powerful built-in support for animations with <Transition> and <TransitionGroup> using both CSS transitions and JavaScript hooks to create smooth, dynamic effects.

By wrapping an element with a <Transition> component Vue will add multiple classes that we can target.

<template>
  <button @click="show = !show">Toggle</button>
  <Transition>
    <p v-if="show" class="fade">Hello, Vue!</p>
  </Transition>
</template>

<script setup>
import { ref } from "vue";

const show = ref(true);
</script>

<style scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>
  • fade-enter-active & fade-leave-active define the transition duration.
  • fade-enter-from & fade-leave-to control the starting and ending opacity.

For list animations, Vue provides <TransitionGroup>:

<template>
  <button @click="addItem">Add Item</button>
  <TransitionGroup tag="ul" name="list">
    <li v-for="item in items" :key="item">{{ item }}</li>
  </TransitionGroup>
</template>

<script setup>
import { ref } from "vue";

const items = ref([1, 2, 3]);

const addItem = () => {
  items.value.push(items.value.length + 1);
};
</script>

<style scoped>
.list-move {
  transition: transform 0.5s;
}
</style>
  • <TransitionGroup> animates list reordering when items are added or removed.
  • The list-move class applies a smooth movement transition.

11. TypeScript Support

Vue 3 Composition API improves TypeScript support by providing better type inference and autocompletion. Let me sneakily redirect you to one of my recent articles for more information.

Better Vue Components with TypeScript [12 examples]

12. Unit Testing

Lastly, we need to mention unit testing as lately, it is considered an essential part of a professional codebase (as it should).

Modern Vue testing revolves around Vitest (for unit tests) and [**Vue Test Utils**](https://test-utils.vuejs.org/) (for component testing), making it easy to test both logic and UI interactions.

Here’s a simple test for a counter component using Vitest and Vue Test Utils:

<!-- Counter.vue -->
<template>
  <button @click="count++">Count: {{ count }}</button>
</template>

<script setup>
import { ref } from "vue";

const count = ref(0);
</script>
// Counter.test.ts
import { mount } from "@vue/test-utils";
import { describe, it, expect } from "vitest";
import Counter from "@/components/Counter.vue";

describe("Counter.vue", () => {
  it("increments when clicked", async () => {
    const wrapper = mount(Counter); // [1]
    const button = wrapper.find("button");

    await button.trigger("click"); // [2]
    expect(button.text()).toBe("Count: 1"); // [3]
  });
});
  1. mount() renders the component in an isolated test environment.
  2. trigger("click") simulates a user interaction.
  3. expect() verifies that the UI updates correctly.

Testing Vue components with Vitest

Conclusion

Vue makes building web apps simple but powerful. It’s easy to learn, yet flexible enough for large projects.

From components and state management to routing and animations, Vue provides everything we need without adding extra complexity. Features like lazy loading and composables keep apps fast, while tools like Pinia and Vue Router help structure our code without getting in our way.

Happy to hear your opinion about Vue 3. Leave a comment below!

Fotis Adamakis

Fotis Adamakis

Senior Software Engineer / Technical Writer

Experienced software engineer writing about front end architecture, accessibility, system design, and developer productivity. Lessons from building and maintaining large-scale frontend applications, with a focus on practical patterns that make codebases easier to understand, scale, and evolve.

Barcelona, Spain