cat blog/.md
Tại sao tôi chọn Vue.js/Nuxt.js thay vì React hay Angular
Trong thế giới frontend, cuộc tranh luận React vs Angular vs Vue chưa bao giờ nguội. Sau nhiều năm làm việc với cả ba, tôi đã chọn Vue.js/Nuxt.js làm stack chính. Không phải vì nó “tốt nhất” — mà vì nó hợp với tôi nhất.
Vấn đề với JSX của React
React là framework tuyệt vời, cộng đồng khổng lồ, ecosystem phong phú. Nhưng mỗi lần viết JSX, tôi luôn cảm thấy có gì đó… không tự nhiên.
HTML trong JavaScript
function UserCard({ user }) {
return (
<div className="card">
{user.isActive && (
<span className="badge">
{user.role === 'admin' ? 'Admin' : 'User'}
</span>
)}
<h2>{user.name}</h2>
{user.skills.length > 0 && (
<ul>
{user.skills.map((skill, index) => (
<li key={index}>{skill}</li>
))}
</ul>
)}
</div>
);
}
Đoạn code trên hoàn toàn hợp lệ, nhưng tôi phải dừng lại vài giây mỗi lần đọc. Logic và markup trộn lẫn vào nhau. Ternary operator lồng nhau. className thay vì class. htmlFor thay vì for. Những thứ nhỏ, nhưng cộng dồn lại thành sự khó chịu.
Cùng logic đó trong Vue:
<template>
<div class="card">
<span v-if="user.isActive" class="badge">
{{ user.role === 'admin' ? 'Admin' : 'User' }}
</span>
<h2>{{ user.name }}</h2>
<ul v-if="user.skills.length > 0">
<li v-for="skill in user.skills" :key="skill">
{{ skill }}
</li>
</ul>
</div>
</template>
HTML vẫn là HTML. Logic điều kiện nằm gọn trong directive. Tôi đọc từ trên xuống và hiểu ngay layout sẽ trông như thế nào.
Hooks và mental model
React Hooks giải quyết được nhiều vấn đề của class components, nhưng lại tạo ra những vấn đề mới:
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []);
Dependency arrays, stale closures, useCallback để tránh re-render không cần thiết… Tôi hiểu tại sao chúng tồn tại, nhưng với một backend developer như tôi, mental model này không trực quan.
Vue 3 Composition API cho cùng logic:
<script setup>
const count = ref(0);
const data = ref(null);
onMounted(() => fetchData().then(d => data.value = d));
watch(count, (val) => document.title = `Count: ${val}`);
const handleClick = () => count.value++;
</script>
Reactivity tường minh. Không dependency array. Không lo stale closure. Code ít hơn, ý đồ rõ hơn.
Vấn đề với cấu trúc file của Angular
Angular là framework enterprise-grade, đầy đủ mọi thứ out of the box. Nhưng cái giá phải trả là quá nhiều file.
Tạo một component đơn giản trong Angular:
user-card/
├── user-card.component.ts
├── user-card.component.html
├── user-card.component.scss
├── user-card.component.spec.ts
└── user-card.module.ts
5 files cho một component hiển thị thông tin user. Rồi còn phải khai báo trong module, import vào module khác. Mở một feature ra, folder tree dài vô tận.
// user-card.component.ts
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.scss']
})
export class UserCardComponent implements OnInit {
@Input() user!: User;
ngOnInit(): void {
// ...
}
}
// user-card.module.ts
@NgModule({
declarations: [UserCardComponent],
imports: [CommonModule],
exports: [UserCardComponent]
})
export class UserCardModule {}
Decorators, dependency injection, RxJS observables, NgModules… Learning curve dốc như núi. Với những dự án enterprise lớn, Angular có lý do tồn tại. Nhưng với phần lớn web apps tôi xây dựng, nó giống như dùng xe tải để đi chợ.
Cùng component đó trong Vue:
<!-- UserCard.vue — 1 file duy nhất -->
<script setup lang="ts">
defineProps<{ user: User }>();
</script>
<template>
<div class="card">
<h2>{{ user.name }}</h2>
</div>
</template>
<style scoped>
.card { /* ... */ }
</style>
1 file. Template, logic, style — tất cả ở một chỗ, nhưng vẫn tách biệt rõ ràng. Single File Component là một trong những thiết kế hay nhất của Vue.
Nuxt.js — Khi Vue gặp convention
Vue giúp tôi viết frontend thoải mái hơn. Còn Nuxt.js thì biến nó thành một trải nghiệm fullstack hoàn chỉnh.
File-based routing
Không cần config router thủ công:
pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/:slug
└── projects.vue → /projects
Tạo file = tạo route. Đơn giản, trực quan, không boilerplate.
Auto-imports
<script setup>
// Không cần import ref, computed, watch, ...
const count = ref(0);
const doubled = computed(() => count.value * 2);
// Không cần import components
// <UserCard /> tự được resolve từ components/
</script>
Nuxt auto-import composables, components, và utilities. Ít import statements = ít noise = tập trung vào logic.
SSR/SSG built-in
Không cần config phức tạp. Nuxt hỗ trợ Server-Side Rendering và Static Site Generation out of the box. Chỉ cần thay đổi một dòng trong nuxt.config.ts:
export default defineNuxtConfig({
ssr: true, // SSR mặc định, đổi thành false nếu muốn SPA
});
Kết hợp với useFetch để gọi API, data sẽ được fetch ở server trước khi gửi HTML về client — tốt cho SEO và performance:
<script setup>
const { data: posts } = await useFetch('https://api.example.com/posts');
</script>
Tôi không nói React hay Angular “tệ”
Mỗi framework có đối tượng riêng:
- React phù hợp nếu bạn thích functional programming, JavaScript-first approach, và cần ecosystem lớn nhất
- Angular phù hợp cho enterprise teams cần convention chặt chẽ, DI, và full-featured framework
- Vue/Nuxt phù hợp nếu bạn muốn sự cân bằng giữa đơn giản và mạnh mẽ, đặc biệt khi đến từ nền tảng backend
Với tôi — một developer PHP/Laravel chuyển sang fullstack — Vue giống như Laravel của frontend: elegant, convention over configuration, progressive. Nó không ép tôi phải thay đổi cách suy nghĩ. Nó adapt theo tôi.
Kết luận
Chọn framework không phải là chọn “cái tốt nhất”. Mà là chọn cái phù hợp nhất với cách bạn nghĩ, cách team bạn làm việc, và loại sản phẩm bạn xây dựng.
Sau 8 năm, tôi đã tìm được stack phù hợp: Laravel ở backend, Nuxt.js ở frontend. Cả hai đều chia sẻ triết lý chung — developer experience trên hết, convention hơn configuration, và elegant code.
Bạn đang dùng framework nào? Lý do chọn nó là gì? Gửi mail cho tôi, mình cùng trao đổi nhé.