How to use the `script setup` syntax in Vue 3

Vue 3.2 introduced the script setup syntax, a slightly less verbose way to declare a component. You enable it by adding a setup attribute to the script element of your SFC, and you can then remove a bit of boilerplate in your component.

Let’s take a practical example, and migrate it to this syntax!

Migrate a component

The following Pony component has two props (the ponyModel to display, and a isRunning flag). Based on these two props, a URL is computed for the image of the pony displayed in the template (via another Image component). The component also emits a selected event when the user clicks on it.

Pony.vue

<template>
  <figure @click="clicked()">
    <Image :src="ponyImageUrl" :alt="ponyModel.name" />
    <figcaption>{{ ponyModel.name }}</figcaption>
  </figure>
</template>
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
import Image from './Image.vue';
import { PonyModel } from '@/models/PonyModel';

export default defineComponent({
  components: { Image },

  props: {
    ponyModel: {
      type: Object as PropType<PonyModel>,
      required: true
    },
    isRunning: {
      type: Boolean,
      default: false
    }
  },

  emits: {
    selected: () => true
  },

  setup(props, { emit }) {
    const ponyImageUrl = computed(() => `/pony-${props.ponyModel.color}${props.isRunning ? '-running' : ''}.gif`);

    function clicked() {
      emit('selected');
    }

    return { ponyImageUrl, clicked };
  }
});
</script>

As a first step, add the setup attribute to the script element. Then, we just need to keep the content of the setup function: all the boilerplate can go away. You can remove the defineComponent and setup functions inside script:

Pony.vue

<script setup lang="ts">
import { computed, PropType } from 'vue';
import Image from './Image.vue';
import { PonyModel } from '@/models/PonyModel';

components: { Image },

props: {
  ponyModel: {
    type: Object as PropType<PonyModel>,
    required: true
  },
  isRunning: {
    type: Boolean,
    default: false
  }
},

emits: {
  selected: () => true
},

const ponyImageUrl = computed(() => `/pony-${props.ponyModel.color}${props.isRunning ? '-running' : ''}.gif`);

function clicked() {
  emit('selected');
}

return { ponyImageUrl, clicked };
</script>

Implicit return

We can also remove the return at the end: all the top-level bindings declared inside a script setup (and all imports) are automatically available in the template. So here ponyImageUrl and clicked are available without needing to return them.

This is the same for the components declaration! Importing the Image component is enough, and Vue understands that it is used in the template: we can remove the components declaration.

Pony.vue

<script setup lang="ts">
import { computed, PropType } from 'vue';
import Image from './Image.vue';
import { PonyModel } from '@/models/PonyModel';

props: {
  ponyModel: {
    type: Object as PropType<PonyModel>,
    required: true
  },
  isRunning: {
    type: Boolean,
    default: false
  }
},

emits: {
  selected: () => true
},

const ponyImageUrl = computed(() => `/pony-${props.ponyModel.color}${props.isRunning ? '-running' : ''}.gif`);

function clicked() {
  emit('selected');
}
</script>

We’re nearly there: we now need to migrate the props and emits declarations.

defineProps

Vue offers a defineProps helper that you can use to define your props. It’s a compile-time helper (a macro), so you don’t need to import it in your code: Vue automatically understands it when it compiles the component.

defineProps returns the props:

const props = defineProps({
  ponyModel: {
    type: Object as PropType<PonyModel>,
    required: true
  },
  isRunning: {
    type: Boolean,
    default: false
  }
});

defineProps receives the former props declaration as a parameter. But we can do even better for TypeScript users!

defineProps is generically typed: you can call it without a parameter, but specify an interface as the “shape” of the props. No more horrible Object as PropType<Something> to write! We can use proper TypeScript types, and add ? to mark a prop as not required 😍.

const props = defineProps<{
  ponyModel: PonyModel;
  isRunning?: boolean;
}>();

We lost a bit of information though. In the previous version, we could specify that isRunning had a default value of false. To have the same behavior, we can use the withDefaults helper:

interface Props {
  ponyModel: PonyModel;
  isRunning?: boolean;
}

const props = withDefaults(defineProps<Props>(), { isRunning: false });

The last remaining syntax to migrate is the emits declaration.

defineEmits

Vue offers a defineEmits helper, very similar to the defineProps helper. defineEmits returns the emit function:

const emit = defineEmits({
  selected: () => true
});

Or even better, with TypeScript:

const emit = defineEmits<{
  (e: 'selected'): void;
}>();

The full component declaration is 10 lines shorter. Not a bad reduction for a ~30 lines component! It’s easier to read, and plays better with TypeScript. It does feel a bit weird to have everything automatically exposed to the template, without writing return though, but you get used to it.

Pony.vue

<template>
  <figure @click="clicked()">
    <Image :src="ponyImageUrl" :alt="ponyModel.name" />
    <figcaption>{{ ponyModel.name }}</figcaption>
  </figure>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import Image from './Image.vue';
import { PonyModel } from '@/models/PonyModel';

interface Props {
  ponyModel: PonyModel;
  isRunning?: boolean;
}

const props = withDefaults(defineProps<Props>(), { isRunning: false });

const emit = defineEmits<{
  (e: 'selected'): void;
}>();

const ponyImageUrl = computed(() => `/pony-${props.ponyModel.color}${props.isRunning ? '-running' : ''}.gif`);

function clicked() {
  emit('selected');
}
</script>

Closed by default and defineExpose

There is a more subtle difference between the two ways to declare components: a script setup component is “closed by default”. This means other components don’t see what’s defined inside the component.

For example, the Pony component can access the Image component (by using refs, as we’ll see in a following chapter). If Image is defined with defineComponent, then everything returned by the setup function is also visible for the parent component (Pony). If Image is defined with script setup, then nothing is visible for the parent component. Image can pick what is exposed by adding a defineExpose({ key: value }) helper. Then the exposed value will be accessible as key.

This syntax is now the recommended way to declare your components, and it’s awesome to use!

Our ebook, online training and training are up-to-date with these changes if you want to learn more!



blog comments powered by Disqus