home / blog / Vue.js Tips That Will Make You a More Pr...
JavaScript Jan 28, 2026

Vue.js Tips That Will Make You a More Productive Developer

A collection of practical Vue.js patterns and tips — from composables and provide/inject to performance optimizations — that I wish I knew earlier.

Vue.js Tips That Will Make You a More Productive Developer

Composables: The Heart of Vue 3

If you're still putting all your logic in setup() or using Options API for complex components, composables will change your life. They're simple functions that encapsulate reactive logic and can be shared across components.

// composables/useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
  const debouncedValue = ref(value.value)

  let timeout
  watch(value, (newValue) => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })

  return debouncedValue
}

// Usage in a component
const search = ref('')
const debouncedSearch = useDebounce(search, 500)

watch(debouncedSearch, (value) => {
  fetchResults(value)
})

A More Practical Composable: API Fetching

// composables/useApi.js
import { ref, shallowRef } from 'vue'

export function useApi() {
  const data = shallowRef(null)
  const error = ref(null)
  const loading = ref(false)

  async function execute(url, options = {}) {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url, {
        headers: { 'Content-Type': 'application/json', ...options.headers },
        ...options,
      })

      if (!response.ok) throw new Error(`HTTP ${response.status}`)

      data.value = await response.json()
      return data.value
    } catch (e) {
      error.value = e.message
      throw e
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, execute }
}

// Usage
const { data: users, loading, execute: fetchUsers } = useApi()
await fetchUsers('/api/users')

Provide/Inject: Skip the Prop Drilling

When you need to pass data through many component layers, provide and inject are cleaner than prop drilling.

// In a parent component
import { provide, ref } from 'vue'

const currentTheme = ref('dark')
const toggleTheme = () => {
  currentTheme.value = currentTheme.value === 'dark' ? 'light' : 'dark'
}

provide('theme', { currentTheme, toggleTheme })

// In any descendant (no matter how deep)
import { inject } from 'vue'

const { currentTheme, toggleTheme } = inject('theme')

v-model with Components: The Right Way

Vue 3's defineModel macro (3.4+) makes custom v-model support trivial:

<!-- BaseInput.vue -->
<script setup>
const model = defineModel()
const props = defineProps({
  label: String,
  type: { type: String, default: 'text' },
})
</script>

<template>
  <div class="form-group">
    <label>{{ label }}</label>
    <input :type="type" v-model="model" class="input" />
  </div>
</template>

<!-- Usage -->
<BaseInput v-model="form.email" label="Email" type="email" />

Performance: Lazy Loading Routes

Don't load your entire app upfront. Split by route:

// router/index.js
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue'),
  },
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue'),
    // Only loads when user navigates to /dashboard
  },
  {
    path: '/settings',
    component: () => import('./views/Settings.vue'),
  },
]

Watchers: Use watchEffect for Simple Cases

// Instead of manually listing dependencies...
watch([firstName, lastName], ([first, last]) => {
  fullName.value = `${first} ${last}`
})

// ...let Vue track them automatically
watchEffect(() => {
  fullName.value = `${firstName.value} ${lastName.value}`
})

Template Refs with TypeScript

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => {
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="inputRef" />
</template>

Quick Tips Roundup

  • Use shallowRef for large objects that don't need deep reactivity — it's significantly faster
  • Prefer computed over methods in templates — computed values are cached
  • Use toRefs when destructuring reactive objects to preserve reactivity
  • Add key attributes to v-for loops with unique IDs, not array indices
  • Use <Suspense> with async components for loading states
  • Keep components under 200 lines — if it's longer, extract a composable or child component

The best Vue code reads like a description of what the UI does, not how it does it. Composables handle the "how," components describe the "what."

back to all posts