index

Vue.js Zen: Writing Clean, Maintainable Code

Photo by jack berry on Unsplash
Photo by jack berry on Unsplash

There’s a certain calm that comes with reading well-written code. No clutter, no confusion, just clarity. Here are some principles I follow to keep my Vue.js projects clean and maintainable.

Keep components small and focused

A component should do one thing well. If you find yourself scrolling endlessly through a single file, it’s time to split it up.

<!-- ❌ Avoid: a component doing too much -->
<template>
  <div>
    <header>...</header>
    <nav>...</nav>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

<!-- ✅ Better: compose from smaller pieces -->
<template>
  <div>
    <AppHeader />
    <AppNav />
    <main><slot /></main>
    <AppFooter />
  </div>
</template>

Use computed properties liberally

Computed properties are cached and reactive. They make your templates cleaner and your logic easier to test.

<script setup>
const props = defineProps(['items'])

// ❌ Avoid: logic in template
// <span>{{ items.filter(i => i.active).length }}</span>

// ✅ Better: computed property
const activeCount = computed(() => 
  props.items.filter(i => i.active).length
)
</script>

<template>
  <span>{{ activeCount }}</span>
</template>

Name things with intention

Good naming eliminates the need for comments. Be explicit, even if it means longer names.

// ❌ Vague
const data = ref([])
const flag = ref(false)
const handle = () => { ... }

// ✅ Clear
const userList = ref([])
const isModalOpen = ref(false)
const submitContactForm = () => { ... }

A few conventions worth adopting: prefix booleans with is, has, or should. Prefix event handlers with on or handle. Name composables with use.

Embrace the Composition API

The Composition API with <script setup> is concise and encourages better code organization. Group related logic together instead of scattering it across options.

<script setup>
// 🔹 User data
const user = ref(null)
const isLoading = ref(false)
const fetchUser = async (id) => { ... }

// 🔹 Form state
const formData = reactive({ name: '', email: '' })
const errors = ref({})
const validateForm = () => { ... }
</script>

When logic grows complex, extract it into composables.

Extract reusable logic into composables

Composables are simple functions that encapsulate reactive state and logic. They keep your components lean.

// composables/useFetch.js
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const isLoading = ref(true)

  fetch(url)
    .then(res => res.json())
    .then(json => data.value = json)
    .catch(err => error.value = err)
    .finally(() => isLoading.value = false)

  return { data, error, isLoading }
}
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data: posts, isLoading } = useFetch('/api/posts')
</script>

Props down, events up

This is the foundation of component communication in Vue. Data flows down through props; changes flow up through events. Resist the temptation to mutate props directly.

<!-- Parent -->
<TodoItem 
  :todo="todo" 
  @update="handleUpdate" 
  @delete="handleDelete" 
/>

<!-- Child -->
<script setup>
defineProps(['todo'])
const emit = defineEmits(['update', 'delete'])

const markComplete = () => emit('update', { ...props.todo, done: true })
</script>

Use v-bind and v-on shorthands consistently

Pick a style and stick with it. I prefer shorthands for their brevity.

<!-- Verbose -->
<button v-bind:disabled="isLoading" v-on:click="submit">
  Save
</button>

<!-- Shorthand (preferred) -->
<button :disabled="isLoading" @click="submit">
  Save
</button>

Avoid v-if with v-for

Using v-if and v-for on the same element is inefficient. Filter your data first.

<!-- ❌ Avoid -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
  {{ user.name }}
</li>

<!-- ✅ Better -->
<script setup>
const activeUsers = computed(() => 
  users.value.filter(u => u.isActive)
)
</script>

<template>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</template>

Provide sensible defaults

Make your components easy to use out of the box.

<script setup>
const props = defineProps({
  variant: {
    type: String,
    default: 'primary',
    validator: (v) => ['primary', 'secondary', 'danger'].includes(v)
  },
  size: {
    type: String,
    default: 'md'
  }
})
</script>

Keep templates readable

When a template becomes hard to read, simplify it. Extract complex expressions, use components for repeated patterns, and don’t be afraid of whitespace.

<!-- ❌ Dense and hard to scan -->
<div :class="[isActive ? 'bg-blue-500' : 'bg-gray-200', isLarge ? 'p-8' : 'p-4', hasError && 'border-red-500']">

<!-- ✅ Easier to read -->
<div :class="containerClasses">

<script setup>
const containerClasses = computed(() => [
  isActive.value ? 'bg-blue-500' : 'bg-gray-200',
  isLarge.value ? 'p-8' : 'p-4',
  hasError.value && 'border-red-500'
])
</script>

Clean code isn’t about perfection, it’s about respect. Respect for your future self, your teammates, and anyone else who might read your code. These small habits compound over time into codebases that are a joy to work with.