Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Vue 3 Composition API Guide

By Kokil Thapa | Last reviewed: September 2026

You picked up Vue because templates stay readable and the learning curve stays sane. Then your components grew past 300 lines, mixins collided, and TypeScript felt bolted on. The Vue 3 Composition API guide you actually need starts there: it replaces the Options API's scattered data, methods, and mounted blocks with grouped, reusable logic inside <script setup>. On production Laravel apps I maintain, that shift cut duplicate booking-form code and made Livewire-plus-Vue islands easier to reason about. This walkthrough covers setup through composables, with patterns you can ship this week.

For Laravel-backed SPAs and partial Vue islands, see the Vue with Laravel setup guide. If you type your frontends, pair this page with TypeScript with Vue 3 best practices.

What is the Vue 3 Composition API and how does it differ from Options API?

The Composition API is a set of importable functions for declaring reactive state, derived values, watchers, and lifecycle hooks. You call them inside setup() or, more often, directly in <script setup>. Logic that belongs together—fetching a user profile, validating a form, syncing query params—sits in one block instead of three separate option keys.

The Options API is still valid. Vue 3 supports both in the same codebase. New feature work on teams I work with almost always uses Composition API because composables replace mixins cleanly and IDE autocomplete improves with typed refs.

Options API vs Composition APIOptions APIdata() — scattered statemethods — far from datamounted — lifecycle splitMixins cause naming clashesComposition APIFeature block: refs + fetchcomputed + watch togetheruseBookingForm()shared composable logicTree-shaken, typed importsmigrate
Vue 3 Composition API guide: feature-grouped logic replaces option-type scattering from the Options API
CriteriaOptions APIComposition API
Logic groupingSplit across data, methods, computedGrouped by feature or domain
Code reuseMixins with implicit naming conflictsComposables with explicit exports
TypeScriptWorks, but inference is weakerStronger inference with typed refs
Bundle sizeEntire option object shapeTree-shakeable function imports
Learning curveLower for small componentsSteeper start, pays off past ~150 lines
Vue 3 default for new codeSupported, not recommended for greenfieldRecommended in official docs

The official Composition API FAQ from Vue.js states both APIs are first-class. Pick Composition API when components carry multiple concerns—search filters plus pagination plus export, typical on admin dashboards and booking UIs.

When Options API still makes sense

Keep Options API for tiny presentational components: a badge, a static card, a one-field toggle. A 40-line Options component is often clearer than the same file with five imports. Migrate incrementally; Vue does not force a full rewrite.

How do you set up a Vue 3 project with the Composition API?

Greenfield Vue 3 apps use Vite 8.x and npm 12. Laravel 13 projects typically embed Vue via Vite in resources/js. The Composition API needs no extra plugin—it ships with Vue 3.

Scaffold with Vite

npm create vue@latest my-app
cd my-app
npm install
npm run dev

Select TypeScript if your team uses it. Choose "Composition API" when the CLI asks—this generates <script setup> stubs. Node.js 26 LTS is the current LTS target for frontend tooling in 2026.

Minimal script setup component

<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}

onMounted(() => {
  console.log('mounted with count', count.value)
})
</script>

<template>
  <button @click="increment">{{ count }} (×2 = {{ doubled }})</button>
</template>

Everything declared at the top level of <script setup> is exposed to the template automatically. No return statement. No export default { setup() {} } boilerplate.

Script Setup Execution FlowImport APIsCreate refsreactive stateRun setup()Render DOMReactivity loopUser eventref.value changeTemplate re-render
Script setup runs once; ref changes trigger targeted template updates through Vue's proxy reactivity

Laravel Vite entry point

On a Laravel 12 or 13 app with PHP 8.3+, register your root component in resources/js/app.js:

import { createApp } from 'vue'
import BookingWidget from './components/BookingWidget.vue'

const el = document.getElementById('booking-widget')
if (el) {
  createApp(BookingWidget, {
    trekId: el.dataset.trekId,
  }).mount(el)
}

Pass server-rendered data through data-* attributes or a small JSON blob in a <script type="application/json"> tag. Never embed secrets in the DOM. For deeper integration patterns, read the Vue 3 Composition API deep dive on this site.

Which Composition API functions should you use for state, computed values, and side effects?

Vue splits reactivity into primitives. Pick the wrong one and you lose reactivity silently—a common production bug.

ref versus reactive

  • ref(value) — wraps any value; access via .value in script, auto-unwrapped in templates. Use for primitives and when you may replace the whole object.
  • reactive(object) — deep proxy for objects only. Cannot reassign the root reference. Fine for fixed-shape form objects.
  • computed(fn) — cached derived state. Use for filtered lists, totals, validation summaries.
  • watch(source, callback) — run side effects when data changes. Use for API calls, localStorage sync, route reactions.
  • watchEffect(fn) — auto-tracks dependencies inside the callback. Use sparingly; explicit watch is easier to debug.
<script setup>
import { ref, reactive, computed, watch } from 'vue'

const search = ref('')
const filters = reactive({ category: 'all', inStock: true })

const filteredProducts = computed(() => {
  return products.value.filter(p => {
    if (filters.inStock && !p.inStock) return false
    if (filters.category !== 'all' && p.category !== filters.category) return false
    return p.name.toLowerCase().includes(search.value.toLowerCase())
  })
})

watch(search, (newVal) => {
  history.replaceState(null, '', `?q=${encodeURIComponent(newVal)}`)
})
</script>

The Vue reactivity core API reference documents edge cases: ref unwrapping in reactive objects, toRef, and toRefs for destructuring without breaking proxies.

Lifecycle and side-effect hooks

  1. onMounted — DOM ready; fetch initial data, attach non-Vue listeners.
  2. onUpdated — after DOM patch; use rarely, prefer watchers.
  3. onUnmounted — cleanup intervals, abort fetch controllers, remove listeners.
  4. onBeforeUnmount — last chance to persist draft state.
import { onMounted, onUnmounted } from 'vue'

let controller = null

onMounted(async () => {
  controller = new AbortController()
  const res = await fetch('/api/treks', { signal: controller.signal })
  treks.value = await res.json()
})

onUnmounted(() => {
  controller?.abort()
})

Always abort in-flight fetches when the user navigates away. I've seen ghost toasts and race-condition bugs on booking widgets without this step.

Props, emits, and defineModel

<script setup>
const props = defineProps({
  trekId: { type: Number, required: true },
  currency: { type: String, default: 'NPR' },
})

const emit = defineEmits(['booked', 'cancel'])

const guestCount = defineModel('guestCount', { type: Number, default: 1 })
</script>

defineModel (Vue 3.4+) replaces verbose v-model prop-plus-emit pairs. For complex forms, validate props with the same rules you enforce server-side in Laravel Form Requests.

How do you organize reusable logic with composables in Vue 3?

A composable is a function named useSomething that calls Composition API functions and returns refs, methods, or computed values. It replaces mixins without magic property merging.

Composables ArchitectureuseBookingForm()composable moduleTrekList.vueimports composableCheckout.vuesame shared logicAdminEdit.vueno mixin clashReturns: { form, errors, submit, reset }Each caller gets isolated state unless you pass a shared store
Composables export explicit APIs—multiple components share booking logic without mixin namespace collisions

Example: useApi composable

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

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

  async function execute(options = {}) {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url, options)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      data.value = await res.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  return { data, error, loading, execute }
}
<script setup>
import { useApi } from '@/composables/useApi'

const { data: treks, loading, error, execute } = useApi('/api/treks')

execute()
</script>

Use shallowRef for large API payloads. Deep reactivity on thousand-row arrays costs memory and CPU. Pair API composables with Laravel Sanctum cookie auth or Bearer tokens as described in Laravel API best practices.

Pinia for shared global state

Composables isolate per-component state. When many routes need the same cart or auth session, use Pinia—a store designed for Composition API:

// stores/cart.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))
  function addItem(product) { items.value.push(product) }
  return { items, total, addItem }
})

On the Quick And Easy Nepalese Grocery Laravel storefront, Pinia plus composables kept delivery-zone logic out of Vue components. The pattern maps cleanly to multi-zone eCommerce builds in the portfolio.

Naming and file conventions

  • Prefix composables with useuseDebounce, useLocalStorage.
  • One composable per file under resources/js/composables/.
  • Return plain objects; destructure in components knowing destructured refs stay reactive only with storeToRefs for Pinia.
  • Test composables with Vitest by calling the function directly—no mount required.

Debug JSON responses from your API endpoints with the site JSON formatter tool before wiring them into composables.

How does the Vue 3 Composition API work with Laravel and production apps?

Most of my Vue work sits inside Laravel Blade apps, not standalone SPAs. Vue islands handle interactive slices: date pickers with Bikram Sambat conversion, document upload progress, live search on lawyer directories. The Composition API keeps each island self-contained.

Laravel + Vue 3 Production StackBlade layoutSEO + SSR shellVite 8 buildVue SFC compileVue islandsscript setupSanctum APIJSON endpointsDeploy pipelineGitLab CInpm run buildDeployer 7Commit built assets when server has no Node.js
Vue 3 Composition API components compile through Vite into Laravel Blade pages backed by Sanctum-protected APIs

Security and XSS

Never bind unsanitized HTML with v-html on user content. Laravel escapes Blade output by default; Vue does not. Read XSS prevention with Blade and Vue before shipping rich text from CMS fields.

Routing with Vue Router 4

Full SPAs mount Vue Router at the Laravel catch-all route. Composition API components use useRoute and useRouter from vue-router. Advanced lazy-loading and guard patterns live in the Vue Router 4 advanced patterns article.

Performance checklist

  1. Lazy-load heavy components with defineAsyncComponent.
  2. Prefer v-show over v-if for toggles that flip often.
  3. Virtualise long lists with a virtual scroller instead of rendering 2,000 DOM nodes.
  4. Commit production Vite builds; do not run Node on shared hosting.
  5. Validate bundle size in CI—Composition API tree-shaking helps, but importing all of lodash does not.

For performance audits on live sites, see speed optimization services and testing and optimization.

Real project pattern: booking widget

On a trekking booking Laravel app with Livewire for admin and Vue for the public date picker, the Composition API composable useAvailability centralised fetch logic, debounced calendar clicks, and NPR/USD price display. Server rules—minimum group size, blackout dates—still validate in Laravel controllers. The widget only handles UX state.

That split mirrors work on Adventure Third Pole Trek and similar booking platforms. Need a team to wire this up? Review web development services or custom software development.

Date handling across Nepali and Gregorian calendars belongs on the server or in tested utilities. For client-side experiments, compare approaches in JavaScript date handling with the Temporal API. Use the Nepali date converter to verify conversions against known BS dates.

Validate regex for slug and phone fields with the regex tester before embedding patterns in composables. For API contract details behind your composables, see building RESTful APIs with Laravel and API development services.

Legal-tech portals like Court Marriage In Nepal use Vue sparingly—lead forms and document upload progress—while Blade carries SEO content. That hybrid keeps Core Web Vitals stable and crawl budget focused on informational pages, a pattern covered in technical SEO services.

More background on the author approach to full-stack delivery lives on the about page. Browse the full project portfolio for Laravel plus Vue combinations across eCommerce, legal, and travel verticals.

Key Takeaways

  • Use <script setup> and Composition API functions for any component past roughly 150 lines or with multiple concerns.
  • Prefer ref for most state; reach for reactive only when the object shape is stable and you will not reassign the root.
  • Extract repeated logic into use* composables instead of mixins; use Pinia when state must span routes.
  • Abort fetches on unmount, use shallowRef for large lists, and never trust client validation alone on Laravel backends.
  • Build Vue assets with Vite 8.x via npm 12, commit artefacts for Deployer-style servers, and mount Vue as islands inside Blade for SEO-heavy sites.
  • Read the official Vue docs alongside Laravel API and XSS guides before shipping user-generated content to the DOM.

People Also Ask

Is the Composition API required in Vue 3?

No. Vue 3 fully supports the Options API. The Composition API is recommended for new components because composables scale better and TypeScript integration is stronger. You can mix both styles in one project during migration.

Should beginners learn Options API or Composition API first?

Learn Composition API with <script setup> first in 2026. Official Vue docs centre on it. Understand Options API enough to maintain legacy code—you will encounter it in older tutorials and codebases.

What is the difference between setup() and script setup?

setup() is a function inside export default that returns data for the template. <script setup> is syntactic sugar: top-level bindings auto-expose to the template and reduce boilerplate. New code should use <script setup> unless you need a specific Options API merge edge case.

Can you use Vue 3 Composition API without TypeScript?

Yes. Plain JavaScript works. TypeScript adds prop typing, composable return types, and safer ref inference. For greenfield apps, enable TypeScript at scaffold time—the incremental cost is low and pays off in composable-heavy codebases.

Ship Vue 3 components that survive production

This Vue 3 Composition API guide gives you the primitives: script setup, refs, composables, and Laravel integration patterns that hold up after deploy. Start one component—extract one composable—then expand. If you want help wiring Vue islands into a Laravel app, booking flow, or eCommerce cart, contact us to discuss architecture, build, and deployment on your stack.

Frequently Asked Questions

Importable Vue 3 functions—ref, reactive, computed, watch—for reactive state, derived values, watchers, and lifecycle hooks inside setup() or script setup, grouped by feature instead of option keys.

No. Vue 3 supports both APIs in one codebase. Composition API is recommended for new components; Options API stays valid, especially for small presentational pieces.

Switch when components exceed roughly 150 lines or combine multiple concerns—filters, pagination, export—where logic scattered across data, methods, and mounted blocks becomes hard to maintain.

Greenfield apps use Vite 8.x and npm 12 with Node.js 26 LTS. Run npm create vue@latest, select Composition API when prompted for script setup stubs. No extra plugin is needed—the API ships with Vue 3. On Laravel 12 or 13 with PHP 8.3+, embed Vue via Vite in resources/js, register root components in app.js, and mount to DOM elements. Pass server data through data- attributes or a JSON script tag. Never embed secrets in the DOM.

ref(value) wraps any value and requires .value in script while auto-unwrapping in templates—use for primitives or when replacing whole objects. reactive(object) applies a deep proxy to objects only and cannot reassign the root reference—fine for fixed-shape forms. Picking the wrong primitive loses reactivity silently, a common production bug I've seen on booking widgets. Prefer ref for most state; reach for reactive only when the object shape is stable and you will not reassign the root.

Keep Options API for tiny presentational components—a badge, static card, or one-field toggle. A 40-line Options component is often clearer than the same file with five imports. Vue supports incremental migration without a full rewrite. Use Composition API when components carry multiple concerns typical on admin dashboards and booking UIs. Official Vue docs recommend Composition API for greenfield work, but both APIs remain first-class and can coexist in one project during migration.

A composable is a useSomething function calling Composition API primitives and returning refs, methods, or computed values. Unlike mixins with implicit naming conflicts, composables export explicit APIs multiple components import safely. Place one composable per file under resources/js/composables/, prefix with use, and return plain objects. Test with Vitest by calling the function directly without mounting. On production Laravel apps I maintain, composables cut duplicate booking-form code that mixins previously tangled across unrelated components.

Most production work mounts Vue as islands inside Blade pages rather than full SPAs. Register components in resources/js/app.js with createApp, mount to DOM elements, and pass trekId or similar via data- attributes. Interactive slices—date pickers, document upload progress, live search—stay self-contained while Blade carries SEO content. Pair composables with Laravel Sanctum cookie auth or Bearer tokens. Server rules like minimum group size still validate in Laravel controllers; Vue handles UX state only. Commit Vite build artefacts for Deployer-style servers without Node on production.

computed(fn) provides cached derived state—filtered lists, totals, validation summaries that depend on reactive inputs. watch(source, callback) runs side effects when data changes—API calls, localStorage sync, route query updates. watchEffect auto-tracks dependencies but is harder to debug; prefer explicit watch in production. Example from the article: computed filters products by search and category; watch syncs search to the URL with history.replaceState. Use computed for what to display; watch for what to do when values change.

script setup is syntactic sugar running Composition API code at the component top level. Everything declared is auto-exposed to the template—no return statement, no export default setup boilerplate. Script setup runs once; ref changes trigger targeted template updates through Vue's proxy reactivity. Official scaffolding selects it when you choose Composition API in npm create vue@latest. Teams I work with use it for new feature work because IDE autocomplete improves with typed refs compared to Options API, especially past components with multiple concerns.

Use defineProps for incoming data with types and defaults—trekId as a required Number, currency defaulting to NPR. defineEmits declares events like booked and cancel. defineModel, available in Vue 3.4+, replaces verbose v-model prop-plus-emit pairs for two-way bindings such as guestCount. Validate props with the same rules enforced server-side in Laravel Form Requests. Never rely on client validation alone; Laravel backends must still enforce business rules on every submission regardless of what the Vue widget displays.

onMounted runs when the DOM is ready—fetch initial data, attach non-Vue listeners. onUnmounted cleans up intervals, aborts fetch controllers, and removes listeners. onBeforeUnmount is the last chance to persist draft state. onUpdated fires after DOM patch but use rarely; prefer watchers instead. Always abort in-flight fetches on unmount—I've seen ghost toasts and race-condition bugs on booking widgets without AbortController cleanup. Create the controller in onMounted and call controller.abort() in onUnmounted.

Composables isolate per-component state—a useApi composable fetching treks in one widget. When many routes need shared cart or auth session, use Pinia stores defined with defineStore and Composition-style setup functions returning refs and computed values. On the Quick And Easy Nepalese Grocery Laravel storefront, Pinia plus composables kept delivery-zone logic out of Vue components. Destructure Pinia state with storeToRefs to keep reactivity; plain destructuring breaks refs. Composables for reusable logic; Pinia for global cross-route state.

Use shallowRef for large API payloads instead of deep reactivity on thousand-row arrays—it costs memory and CPU. Lazy-load heavy components with defineAsyncComponent. Prefer v-show over v-if for toggles that flip often. Virtualise long lists instead of rendering 2,000 DOM nodes. Validate bundle size in CI—Composition API tree-shaking helps, but importing all of lodash negates those gains. Commit production Vite builds rather than running Node on shared hosting. Abort fetches on unmount to prevent race conditions updating stale components.

Never bind unsanitized user HTML with v-html—Laravel escapes Blade output by default but Vue does not. Read XSS prevention guidance before shipping rich text from CMS fields to the DOM. For reactivity, destructuring refs from composables keeps them reactive, but plain destructuring from Pinia stores strips proxies—use storeToRefs instead. ref unwrapping inside reactive objects and toRef or toRefs help destructuring without breaking proxies. Silent reactivity loss is a recurring production bug when developers spread or destructure state assuming proxies survive.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: