iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
😎

[Vue.js 3.2] The <script setup> syntax is amazing

に公開

What is the <script setup> Syntax?

Starting from Vue.js 3.2, the <script setup> syntax became available. This is syntactic sugar for using the Composition API within Single File Components (SFCs). It offers the following benefits and is recommended by the official documentation:

  • More concise code with less boilerplate
  • Ability to use pure TypeScript syntax when defining props and emits
  • Better runtime performance
  • Better IDE performance

Basic Syntax

To explain the <script setup> syntax simply, it allows you to write the contents of the conventional Composition API's setup() function directly inside the <script> tag.

You can use this syntactic sugar by adding the setup attribute to the <script> tag of a Single File Component.

Let's compare how much more concise the writing has become with the <script setup> syntax by looking at the Options API and the Composition API styles.

  • Options API
<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
  },
});
</script>

It's a simple counter app, but it would look roughly like this.
The following is what happens when you replace this with the Composition API.

  • Composition API
<template>
  <h1>{{ count }}</h1>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    const count = ref(0);

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

    const decrement = () => {
      count.value--;
    };
    return {
      count,
      increment,
      decrement,
    };
  },
});
</script>

Finally, here is the long-awaited <script setup> syntax.

  • <script setup>
<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

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

const decrement = () => {
  count.value--;
};
</script>

<template>
  <h1>{{ count }}</h1>
  <button @click="increment">Increment</button>
  <button @click="decrement">Decrement</button>
</template>

Is this Svelte!? Even in the official sample code, the order has changed from <template><script> to <script><template>.

It feels great to be able to write using pure JavaScript syntax with fewer Vue.js-specific constructs.

When comparing it with the Composition API, we can see the following differences:

  • You no longer need to export a Vue.js object using export default.
  • Variables and functions defined within the setup() function had to be returned to be used in the <template>, but when declared within <script setup>, everything becomes automatically available.

There are several other differences from the traditional way of writing, so let's take a look.

VSCode Extension

Vetur was probably the most commonly used VSCode extension for .vue files, but it does not support the <script setup> syntax.

Instead, using Volar is now recommended.

https://twitter.com/vuejs/status/1429195877365780486

Components

Previously, you had to register a list of imported components in the components option, but now they are available for use directly just by importing them.

<script lang="ts" setup>
import TheHeader from "./components/TheHeader.vue";
import TheFooter from "./components/TheFooter.vue";
</script>

<template>
  <TheHeader />
  <main>main</main>
  <TheFooter />
</template>

While the kebab-case <the-header> can still be used, the official documentation seems to strongly recommend PascalCase <TheHeader>.

The kebab-case equivalent <my-component> also works in the template - however PascalCase component tags are strongly recommended for consistency. It also helps differentiating from native custom elements.

https://v3.vuejs.org/api/sfc-script-setup.html#using-components

Additionally, auto-import is also working quite well.

setup-component

Namespaced Components

You can do something similar to React.

  • Forms/Input.vue
<template>
  <input type="text" />
</template>
  • Forms/Checkbox.vue
<template>
  <input type="checkbox" />
</template>
  • Forms/index.ts
export { default as Input } from "./Input.vue";
export { default as Checkbox } from "./Checkbox.vue";
  • App.vue
<script lang="ts" setup>
import * as Form from "./components/Forms";
</script>

<template>
  <Form.Input />
  <Form.Checkbox />
</template>

However, in this case, the components result in an any type, so it's unclear whether this is the correct approach or not 🤔

Screenshot 2021-09-21 21.26.07

Props and emit

This is the part I was personally most excited about. You can now define props and emit using pure TypeScript.

Props

<script setup lang="ts">
import { computed } from "@vue/reactivity";

interface Props {
  value: string;
  label?: string;
  type?: "text" | "password" | "email" | "number";
  placeholder?: string;
  disabled?: boolean;
}

const props = defineProps<Props>();

const disabledClass = computed(() => {
  props.disabled ? "bg-gray-200" : "";
});
</script>
<template>
  <label>
    {{ label }}
    <input
      :class="disabledClass"
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
    />
  </label>
</template>

Props are defined using defineProps(). defineProps, as well as defineEmits and withDefaults mentioned later, are compiler macros that can be used within <script setup>, and there is no need to import them from anywhere.

When using TypeScript, defineProps() can accept type arguments, and the passed type definition can be used instead of traditional property validation.

Types such as string and boolean correspond to the parts that were previously defined using String or Boolean in the conventional type option.

Making a property optional is equivalent to the traditional required: false, and if it is not optional, it is equivalent to required: true.

The type definitions here also enable type hints when using the component.

To define default values for props, use withDefaults.

const props = withDefaults(defineProps<Props>(), {
  label: "",
  type: "text",
  placeholder: "",
  disabled: false,
});

emit

Like props, emits can also be type-defined using TypeScript.

<script setup lang="ts">
interface Emits {
  (e: "input", value: string): void;
  (e: "update:value", value: string): void;
}

const emit = defineEmits<Emits>();

const handleInput = ({ target }: { target: HTMLInputElement }) => {
  emit("input", target.value);
  emit("update:value", target.value);
};
</script>

<template>
  <label>
    {{ label }}
    <input
      :class="disabledClass"
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
      @input="handleInput"
    />
  </label>
</template>

Of course, typing is correctly applied.

Screenshot 2021-09-21 21.28.04

Screenshot 2021-09-21 21.41.06

useSlots and useAttrs

In the Composition API, $slots and $attrs were obtained from the second argument of the setup() function. In <script setup>, useSlots() and useAttrs() are used instead.

<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

Using with Normal <script>

<script setup> and a normal <script> can be defined simultaneously in a single Single File Component. This is used for the following purposes:

  • Defining options that cannot be set in <script setup>, such as the inheritAttrs option.
  • When you want to execute operations with side effects only once (<script setup> is executed every time a component is created).
  • When you want to perform a named export.
<script setup lang="ts">
interface Props {
  value: string;
  label?: string;
  type?: "text" | "password" | "email" | "number";
  placeholder?: string;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  label: "",
  type: "text",
  placeholder: "",
  disabled: false,
});
</script>

<script lang="ts">
import { defineComponent } from "@vue/runtime-core";

export default defineComponent({
  inheritAttrs: false,
});
</script>

<template>
  <label>
    {{ label }}
    <input
      :value="value"
      :type="type"
      :placeholder="placeholder"
      :disabled="disabled"
    />
  </label>
</template>

Top-level await

Top-level await can be used within <script setup>.

  • AsyncUserList.vue
<script setup lang="ts">
import { ref } from "@vue/reactivity";

interface User {
  id: number;
  username: string;
}

const result = await fetch("https://jsonplaceholder.typicode.com/users");
const json = await result.json();

const users = ref<User[]>(json);
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.username }}
    </li>
  </ul>
</template>

One thing to note is that as soon as await is used, that component is treated as an asynchronous component that returns a Promise.

In other words, it must be used in combination with <Suspense>.

For more details, please refer to the following:

https://v3.ja.vuejs.org/guide/migration/suspense.html#他のコンポーネントとの組み合わせ

  • App.vue
<script lang="ts" setup>
import AsyncUserList from "./components/AsyncUserList.vue";
</script>

<template>
  <Suspense>
    <AsyncUserList />
  </Suspense>
</template>

Although I am introducing it, since <Suspense> itself is an experimental feature, it may be better to refrain from using top-level await for now.

Impressions

For better or worse, it feels like Vue.js is moving away from its traditional writing style and incorporating the good points of other frameworks. There is no doubt that it has become more concise than previous methods.

Personally, I really like the fact that I can now define types for props and emits. It feels like the TypeScript experience in Vue.js is continuing to improve.

References

https://v3.vuejs.org/api/sfc-script-setup.html

GitHubで編集を提案

Discussion