PYYUPSK

a self-taught dev from Thailand

Pongsakorn Thipayanate · Samutsakhon, TH ·webring

essay

The Real Vue.js Developer Journey: Wins, Woes & Lessons

This article covers my move from React to Vue.js. It looks at the wins, the friction, and what both ecosystems taught me.

view .mdopen in claudeopen in chatgpt

Moving from React to Vue.js felt disorienting at first. The concepts were familiar, since both use component-based architecture and declarative UI. But small differences kept throwing me off. Vue uses v-for instead of map(). Vue uses ref() and reactive() instead of useState(). Adjusting to these differences took time.

Once I pushed through that friction, Vue started to make sense. I built MVPs and scaled client apps with it, and its design choices became clearer over time. Vue became my choice for projects that needed fast iteration without losing maintainability.

My Vue Setup

My Vue 3 stack, used across multiple client projects:

  • Vite: A fast dev server with HMR (hot module replacement) that keeps iteration cycles under a second.
  • Pinia: State management with a modular, composable design that scales without extra bloat.
  • Vue Router: Flexible client-side navigation that works well with auth flows and lazy loading.
  • TypeScript: Type safety that catches bugs before runtime and keeps codebases maintainable.

I chose this combination after weighing trade-offs between build speed and feature richness. It has its quirks. Here is what I learned while working through them.

The Learning Curve

Coming from React, I had to unlearn a lot. Vue’s reactivity system uses proxies. You use ref() for single values, reactive() for objects, and watch() or watchEffect() for side effects. React re-renders explicitly through state setters. Vue instead updates only what changes, using fine-grained reactivity. This design brings performance gains, but you must understand it well to avoid pitfalls like over-reactivity.

At first, I tried mapping everything to React equivalents. I treated ref() like useState() and computed() like useMemo(). This approach did not work well. Vue follows its own philosophy: declarative rendering, composition over inheritance, and a reactivity model built for data-driven UIs.

What Helped

Building real things helped the most. Vue’s documentation stays clear and includes many interactive examples. Small side projects, such as a dashboard proof of concept and a few component experiments, let me test ideas without client deadlines. I started with the Composition API and built small components before I tackled larger features. This approach made the adjustment manageable.

State Management with Pinia

I had heard about Vuex’s mutation-heavy boilerplate and did not look forward to using it. Then I found Pinia, a lighter reimagining of Vuex built for the Composition API.

Pinia offers a flatter structure, better developer tools integration, including time-travel debugging in Vue Devtools, and solid TypeScript support. Here is a cart store from a recent e-commerce project:

import { defineStore } from "pinia";

export const useCartStore = defineStore("cart", {
  state: () => ({
    items: [],
    loading: false,
    error: null,
  }),

  getters: {
    itemCount: (state) => state.items.reduce((sum, i) => sum + i.quantity, 0),
    totalPrice: (state) =>
      state.items.reduce((sum, i) => sum + i.price * i.quantity, 0),
    discountedTotal: (state) => {
      const total = state.items.reduce(
        (sum, i) => sum + i.price * i.quantity,
        0,
      );
      return total > 100 ? total * 0.9 : total; // 10% discount over $100
    },
  },

  actions: {
    async fetchCart(userId) {
      try {
        this.loading = true;
        this.items = await fetchCartFromAPI(userId);
      } catch (err) {
        this.error =
          err instanceof Error ? err.message : "Oops, something went wrong.";
      } finally {
        this.loading = false;
      }
    },
    addItem(item) {
      const existing = this.items.find((i) => i.id === item.id);
      if (existing) {
        existing.quantity += item.quantity;
      } else {
        this.items.push(item);
      }
    },
  },
});

What works well:

  • TypeScript-friendly: Pinia infers types, so you write fewer manual declarations.
  • Autocomplete in VS Code: This speeds up development and reduces errors.
  • No boilerplate mutations: Actions handle synchronous and asynchronous logic directly.
  • Modular: Plugins add persistence, such as localStorage sync, or undo and redo.

For larger apps, Pinia’s store composition splits state across modules without losing organization.

Routing

Vue Router handles a lot: dynamic routes, nested views, and programmatic navigation. It took time to get comfortable with, especially after coming from Next.js’s file-based routing.

The manual route definitions felt dated at first, but the flexibility pays off for role-based access control or dynamic parameters like /dashboard/:userId/widgets.

My Typical Setup

import { createRouter, createWebHistory } from "vue-router";

import Dashboard from "@/views/Dashboard.vue";
import Home from "@/views/Home.vue";
import Product from "@/views/Product.vue";

const routes = [
  { path: "/", component: Home },
  { path: "/product/:id", component: Product, props: true },
  {
    path: "/dashboard/:userId",
    component: Dashboard,
    meta: { requiresAuth: true },
    children: [
      { path: "widgets", component: () => import("@/components/Widgets.vue") },
    ],
  },
];

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

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    next("/login");
  } else {
    next();
  }
});

export default router;

For faster prototyping, I am now exploring file-based routing through unplugin-vue-router. Nuxt’s convention-over-configuration approach is worth borrowing.

Learn more about advanced routing

Performance

Scaling to large datasets, such as real-time analytics dashboards, exposed real issues. These included slow renders, sluggish updates, and memory leaks that Vue’s virtual DOM does not fix on its own.

What Helped

  • Lazy loading: I use defineAsyncComponent to chunk bundles.
  • computed over watch: I use computed for cached derivations.
  • Dev performance tracking:
import { createApp } from "vue";

import App from "@/App.vue";

const app = createApp(App);

app.config.performance = import.meta.env.DEV;

app.config.compilerOptions = {
  comments: false,
  delimiters: ["${", "}"],
  whitespace: "condense",
};

app.config.errorHandler = (err, instance, info) => {
  console.error(`Error: ${err.toString()}\nInfo: ${info}`);
};

app.mount("#app");

One project saw a 40% improvement in Time to Interactive (TTI). Vue Devtools and Lighthouse audits help track these gains.

Ecosystem Integration

I have integrated Vue with Node/Express and Firebase backends. I use Axios or TanStack Query for data fetching, Vitest for unit tests, and Cypress for end-to-end (E2E) tests. I deploy through Vercel or Netlify.

Vue’s footprint, under 30KB gzipped, makes it practical for micro-frontends or embedded UIs.

The Verdict

What Works

  • Single-file components: These promote encapsulation and reusability.
  • The reactive system: This feels natural once you learn it, and it produces efficient, declarative UIs.
  • Pinia: This balances simplicity with extensibility.
  • Ecosystem synergy: Vite, TypeScript, and Vue Router together form a solid toolchain.

What Does Not Work

  • Setup overhead: TypeScript and routing configuration can slow down initial prototyping.
  • Pinia overkill for simple apps: Sometimes basic ref composables are enough.
  • Eager reactivity: This needs careful handling in data-intensive scenarios.
  • The Options API and Composition API split: This can confuse newcomers while they are learning.

Final Thoughts

Vue went from a curiosity to a reliable choice for me. The syntax adjustment was real. But the framework underneath is solid, giving fast development without the overhead of heavier alternatives.

I still use Next.js when SSR (server-side rendering) or SEO (search engine optimization) is the priority. For lightweight, reactive frontends, especially new projects built from scratch, Vue 3 works well. Start small, build something real, and see how it feels.

← All writings