Vue.js Script Setup

<script setup> is a compile-time syntactic sugar for using the Composition API inside Single File Components (SFCs). It provides a more concise way to write Vue components with better performance and TypeScript support.

Available Functions

Component Communication

  • defineProps() - Define component props with type checking and validation
  • defineEmits() - Define component events for parent-child communication

Additional Script Setup Functions

  • defineExpose() - Expose component methods/properties to parent components
  • useSlots() - Access component slots programmatically
  • useAttrs() - Access component attributes (fallthrough attributes)

Basic Syntax

Traditional vs Script Setup

Traditional Composition API:

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

export default {
  props: ['title'],
  emits: ['update'],
  setup(props, { emit }) {
    const count = ref(0)

    const increment = () => {
      count.value++
      emit('update', count.value)
    }

    return {
      count,
      increment
    }
  }
}
</script>

Script Setup (Modern):

<script setup>
import { ref } from 'vue'

const props = defineProps(['title'])
const emit = defineEmits(['update'])

const count = ref(0)

const increment = () => {
  count.value++
  emit('update', count.value)
}
</script>

Key Benefits

1. Less Boilerplate

  • No need for export default
  • No need to return values from setup
  • Automatic exposure of variables to template

2. Better Performance

  • Variables are compiled to be directly accessible
  • Reduced component instance creation overhead

3. Better TypeScript Support

  • Improved type inference
  • Better IDE support and autocomplete

4. Cleaner Code Organization

<script setup>
// 1. Imports
import { ref, computed, onMounted } from 'vue'
import MyComponent from './MyComponent.vue'

// 2. Props and emits
const props = defineProps({
  title: String,
  count: { type: Number, default: 0 }
})
const emit = defineEmits(['update', 'delete'])

// 3. Reactive data
const localCount = ref(props.count)
const message = ref('')

// 4. Computed properties
const displayTitle = computed(() =>
  props.title ? props.title.toUpperCase() : 'Default Title'
)

// 5. Methods
const handleClick = () => {
  localCount.value++
  emit('update', localCount.value)
}

// 6. Lifecycle hooks
onMounted(() => {
  console.log('Component mounted')
})
</script>

Migration from Options API

Before (Options API)

<script>
export default {
  props: ['title'],
  data() {
    return {
      count: 0
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('mounted')
  }
}
</script>

After (Script Setup)

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

const count = ref(0)

const doubleCount = computed(() => count.value * 2)

const increment = () => {
  count.value++
}

onMounted(() => {
  console.log('mounted')
})
</script>

Best Practices

  1. Organize code logically: imports → props/emits → reactive data → computed → methods → lifecycle
  2. Use TypeScript: Better type safety and IDE support
  3. Keep components focused: Use composables for reusable logic
  4. Validate props: Always define prop types and validation
  5. Use meaningful event names: Make component communication clear

Limitations

  • Cannot use await at the top level (use <Suspense> instead)
  • Cannot conditionally call defineProps or defineEmits
  • Limited support for dynamic component registration