I spent this weekend poking around Vue for the first time—no hand-holding tutorials, just me, a blank editor, and the official docs. After six years of React, I expected some déjà vu 😰. Instead, I found fresh ergonomics, surprising simplicity, and a mental model that feels more… declarative 😇.
Templates: HTML First
Opening App.vue, I stared at plain HTML in a block. Interpolation uses {{ count }}
instead of embedding JSX inside JavaScript. Control flow? Directives like v-if and v-for keep loops and branches in markup, not scattered across ternaries and map() in JSX. It felt like my linters and Prettier could finally focus on real HTML 😅.
<ul>
<li
v-for="item in items"
:key="item.id"
v-show="item.visible"
:class="{ active: item.isActive }"
>
{{ item.label }}
</li>
</ul>
Compare with React:
<ul>
{items.map(item =>
item.visible ? (
<li
key={item.id}
className={item.isActive ? 'active' : ''}
>
{item.label}
</li>
) : null
)}
</ul>
Scoped Styles: Zero Boilerplate
Drop a <style scoped>
block into your SFC and—bam—PostCSS tags your selectors with a unique ID. No CSS modules, no styled-components, no runtime style libs. Your .my-button { … }
only affects this component.
<style scoped>
.my-button { background: hotpink }
</style>
In React, I’d import a CSS module or reach for Styled components (RIP 🥲). Vue treats scoped styles as first-class citizens.
Reactive State: ref() & reactive()
This is where Vue’s mental model clicked for me 😍. Instead of juggling multiple useState hooks, Vue treats your state as plain variables wrapped in reactive containers:
<script setup>
import { ref, reactive } from 'vue'
// Primitive state
const count = ref(0)
// Object/array state
const state = reactive({ items: [] })
// Mutations happen in place
function addItem(item) {
state.items.push(item)
count.value++
}
</script>
<template>
<button @click="addItem('🍎')">
Added {{ count }} item{{ count > 1 ? 's' : '' }}
</button>
</template>
-
ref()
wraps a primitive so Vue can track.value
changes and trigger updates—no setter functions needed. -
reactive()
uses a Proxy under the hood, so mutatingstate.items.push(...)
orstate.items[0] = 'x'
instantly reflects in the DOM.
Vue’s mental model treats state like plain JS objects and arrays—you read and write them directly. The reactivity system and compiler-informed virtual DOM handle dependency tracking and efficient updates for you. It turns state management into “just JavaScript,” without boilerplate or indirection.
What's next?
That’s a wrap for today. Vue’s “just write HTML and JS” vibe is growing on me. The reactive system feels intuitive, the separation of concerns is clean, and I haven’t written a single custom hook yet 😉.
Next up, I’ll go deeper into component communication—passing props, emitting events, and managing local state across a small UI tree. Basically, how Vue handles what React devs usually reach for useContext
or lifting state up to solve. Spoiler: It’s kinda elegant 😌.
Stay tuned, this is getting fun.
Top comments (3)
Don't skip out on Options API. The main feature of using Vue's atomic reactivity functions (
ref
,computed
,watch
, etc) is they allow you to extend reactivity to outside of the component. But the vast majority of the components you'll write will not require that. So you should take advantage of the built in organizational structure the framework gives you. Also use ESLint-Plugin-Vue with all the settings maxed out. The best thing about Vue is being able to go to any component, in any codebase, written by any dev, and know instantly where everything is. No other framework offers that (even though they all should). But the only way to fully get this ability is with Options API and the linter plugin. All the teams at my company do this, and as I jump between their repos constantly as part of my job, it is so nice to just know where everything is without having to study every component to "figure out" whatever that person was thinking when they wrote it. Or to have to have 50 discussions a week to figure out the right way to organize code for this specific repo. What a waste of time, so happy the framework solves this for us.Other things to compare:
Also you can use
<style module="$s">
to enable CSS modules in Vue components. Scoped styles are better for apps, CSS Modules are better for component libraries (for style scoping). But Vue makes all CSS stuff super easy (See:v-bind()
in CSS).Here's an Options API boilerplate with Vue-Router and Pinia already setup:
There is nothing wrong in some self advertisement, but the Options API is slowly being abandoned for a reason, it is objectively verbose and hard to read. When I moved to Vue it helped me better understand what was happening under the hood, but after that I'd strongly discourage it: it needs a lot of memory and gets messy pretty quickly.
The switch to nuxt 3 has the benefit of making you really get rid of irrelevant details and having comments that just describe the essence of the presentation logic in a very clean way (ad the author correctly figured). I'd recommend following on with the series and giving this new approach a try :)
Thank you so much, this is insightful