I'm making a course on Laravel Sanctum: MasteringAuth.com

From options API to script setup using the composition API

Constantin Druc · 24 Feb, 2022

If you are using or want to use the new vue composition API, script setup is now the recommended syntax for it. And that comes with a couple of advantages:

  • less boilerplate
  • better typescript support
  • slight performance benefits

In this post, we’ll take the video player component I use here on tallpad, which is currently written using the options API, and convert it to the new script setup syntax using the new composition API.

The template part is irrelevant as it doesn’t have to change when you switch to the script setup syntax; so I’ll just add an empty tag; just pretend it’s there and it has stuff in it. You’ll find the full snippets by clicking on the “Code” tab above.

1<template>(Click to show)
2 <div class="relative aspect-w-16 aspect-h-9">
3 <div class="flex absolute inset-0 justify-center items-center">
4 <LoadingIcon class="w-14 h-14 text-white animate-spin"></LoadingIcon>
5 </div>
6 
7 <slot></slot>
8 </div>
9 
10 <div class="flex justify-end">
11 <div class="grid grid-cols-5 w-full max-w-xl divide-x divide-gray-700">
12 <button
13 class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
14 @click="toggleSpeed()">
15 <span class="hidden sm:inline-block">Speed:</span><span>{{ currentSpeed }}x</span>
16 </button>
17 
18 <button
19 class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
20 @click="goBack(5)">
21 <BackwardIcon class="w-4 h-4"/>
22 <span>5s</span>
23 </button>
24 
25 <button
26 class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
27 @click="goForward(5)">
28 <span>5s</span>
29 <ForwardIcon class="w-4 h-4"/>
30 </button>
31 
32 <button
33 class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
34 @click="goBack(10)">
35 <BackwardIcon class="w-4 h-4"/>
36 <span>10s</span>
37 </button>
38 
39 <button
40 class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
41 @click="goForward(10)">
42 <span>10s</span>
43 <ForwardIcon class="w-4 h-4"/>
44 </button>
45 </div>
46 </div>
47</template>
48 
49<script>
50import axios from "axios";
51import Player from '@vimeo/player';
52import {throttle} from 'lodash';
53import ForwardIcon from './svgs/ForwardIcon';
54import BackwardIcon from './svgs/BackwardIcon';
55import LoadingIcon from './svgs/LoadingIcon';
56 
57export default {
58 props: {
59 lessonId: Number,
60 trackProgress: Boolean
61 },
62 components: {
63 BackwardIcon,
64 ForwardIcon,
65 LoadingIcon
66 },
67 data() {
68 return {
69 speedIndex: 1,
70 player: null,
71 speedOptions: [0.75, 1, 1.25, 1.5, 1.75, 2.0],
72 }
73 },
74 created() {
75 if (window.localStorage.getItem('speedIndex')) {
76 this.speedIndex = parseInt(window.localStorage.getItem('speedIndex'));
77 }
78 },
79 mounted() {
80 player = new Player(this.$slots.default()[0].el);
81 player.on('play', () => player.setPlaybackRate(this.currentSpeed));
82 
83 if (this.trackProgress) {
84 player.on('progress', throttle((event) => {
85 axios.put(`/lessons/${this.lessonId}/progress`, {percent: event.percent});
86 }, 10000));
87 
88 player.on('ended', (event) => {
89 axios.put(`/lessons/${this.lessonId}/progress`, {percent: 1});
90 });
91 }
92 },
93 unmounted() {
94 player.off('play');
95 player.off('progress');
96 player.off('ended');
97 },
98 methods: {
99 toggleSpeed() {
100 if (typeof speedOptions[this.speedIndex + 1] !== "undefined") {
101 this.speedIndex += 1;
102 } else {
103 this.speedIndex = 0;
104 }
105 
106 window.localStorage.setItem('speedIndex', this.speedIndex);
107 
108 player.setPlaybackRate(this.currentSpeed);
109 },
110 async goForward(seconds) {
111 const currentTime = await player.getCurrentTime();
112 const totalDuration = await player.getDuration();
113 player.setCurrentTime(Math.min(totalDuration, (currentTime + seconds)));
114 },
115 async goBack(seconds) {
116 const currentTime = await player.getCurrentTime();
117 player.setCurrentTime(Math.max(0, (currentTime - seconds)));
118 }
119 },
120 computed: {
121 currentSpeed() {
122 return speedOptions[this.speedIndex];
123 }
124 }
125}
126</script>

You can place the script tag anywhere you want, but the convention seems to be to have it set at the start of the file. So we’ll move it and also add a setup attribute to it.

1<script setup>...</script>
2 
3<template>...</template>

Moving on, to define the component props, we can use the defineProps function, which is a vue compiler macro and doesn’t need to be imported from anywhere, and which takes the props object as an argument. We’ll store that into a props constant to be used later.

1const props = defineProps({
2 lessonId: Number,
3 trackProgress: Boolean
4});
5 
6export default {
7 props: {
8 lessonId: Number,
9 trackProgress: Boolean
10 }
11 //...
12}

Now, for my favorite part of this new script setup syntax, we no longer need to register the components, so we can completely delete the components object. As long as you import those components, they will be readily available to use in your template.

1export default {
2 components: {
3 BackwardIcon,
4 ForwardIcon,
5 LoadingIcon
6 },
7 //...
8}

For the data function, we’ll grab the object properties and convert them into reactive values using the ref function.

However, if you take a close look at the initial code snippet, you will find that not all properties should necessarily be reactive. The player and speedOptions values never change. Well, player does change, but that’s just because I need to wait for the component to mount to create a new instance of the Vimeo Player using the default slot element. Other than that, those values never change, so we’ll just turn them into plain, non-reactive javascript values.

1const speedIndex = ref(1);
2let player = null;
3const speedOptions = [0.75, 1, 1.25, 1.5, 1.75, 2.0];
4 
5export default {
6 data() {
7 return {
8 speedIndex: 1,
9 player: null,
10 speedOptions: [0.75, 1, 1.25, 1.5, 1.75, 2.0],
11 }
12 }
13 //...
14}

The created lifecycle method no longer exists in the vue3 composition API. It has been replaced with the setup function. This means we can grab the code under created, paste it directly into our script setup, and replace the old data properties with the new reactive values.

1if (window.localStorage.getItem('speedIndex')) {
2 speedIndex.value = parseInt(window.localStorage.getItem('speedIndex'));
3}
4 
5 
6export default {
7 created() {
8 if (window.localStorage.getItem('speedIndex')) {
9 this.speedIndex = parseInt(window.localStorage.getItem('speedIndex'));
10 }
11 }
12 //...
13}

Moving on, for the mounted lifecycle method, we can use the onMounted hook and then replace the old reactive references using this. for data properties and props. for the component props.

Then, to get the slot element, we'll use the useSlot() helper, and finally, we'll need to convert the `this.currentSpeed` computed property to use the new composition API.

1const speedIndex = ref(1);
2let player = null;
3const speedOptions = [0.75, 1, 1.25, 1.5, 1.75, 2.0];
4const currentSpeed = computed(() => speedOptions[speedIndex.value]);
5 
6onMounted(() => {
7 player = new Player(useSlots().default()[0].el);
8 player.on('play', () => player.setPlaybackRate(currentSpeed.value));
9 
10 if (props.trackProgress) {
11 player.on('progress', throttle((event) => {
12 axios.put(`/lessons/${props.lessonId}/progress`, {percent: event.percent});
13 }, 10000));
14 
15 player.on('ended', () => {
16 axios.put(`/lessons/${props.lessonId}/progress`, {percent: 1});
17 });
18 }
19});
20 
21export default {
22 mounted() {
23 player = new Player(this.$slots.default()[0].el);
24 player.on('play', () => player.setPlaybackRate(this.currentSpeed));
25 
26 if (this.trackProgress) {
27 player.on('progress', throttle((event) => {
28 axios.put(`/lessons/${this.lessonId}/progress`, {percent: event.percent});
29 }, 10000));
30 
31 player.on('ended', (event) => {
32 axios.put(`/lessons/${this.lessonId}/progress`, {percent: 1});
33 });
34 }
35 },
36 //...
37 computed: {
38 currentSpeed() {
39 return speedOptions[this.speedIndex];
40 }
41 }
42}

For the unmounted lifecycle method, we can use the onUnmounted hook, cut and paste everything there and replace the old this. references with the plain variable we defined above.

1onUnmounted(() => {
2 player.off('play');
3 player.off('progress');
4 player.off('ended');
5});
6 
7export default {
8 unmounted() {
9 player.off('play');
10 player.off('progress');
11 player.off('ended');
12 },
13 //...
14}

We can now move on to methods. I’ll cut and paste the first function, add the function keyword, and replace the old references with the new ones.

1function toggleSpeed() {
2 if (typeof speedOptions[speedIndex.value + 1] !== "undefined") {
3 speedIndex.value += 1;
4 } else {
5 speedIndex.value = 0;
6 }
7 
8 window.localStorage.setItem('speedIndex', speedIndex.value);
9 player.setPlaybackRate(currentSpeed.value);
10}
11 
12export default {
13 methods: {
14 toggleSpeed() {
15 if (typeof speedOptions[this.speedIndex + 1] !== "undefined") {
16 this.speedIndex += 1;
17 } else {
18 this.speedIndex = 0;
19 }
20 
21 window.localStorage.setItem('speedIndex', this.speedIndex);
22 
23 player.setPlaybackRate(this.currentSpeed);
24 },
25 }
26 //...
27}

We’ll do the same for the second function:

1async function goForward(seconds) {
2 const currentTime = await player.getCurrentTime();
3 const totalDuration = await player.getDuration();
4 player.setCurrentTime(Math.min(totalDuration, (currentTime + seconds)));
5}
6 
7export default {
8 methods: {
9 async goForward(seconds) {
10 const currentTime = await player.getCurrentTime();
11 const totalDuration = await player.getDuration();
12 player.setCurrentTime(Math.min(totalDuration, (currentTime + seconds)));
13 },
14 }
15 //...
16}

And the third; and we can completly delete the old export.

1async function goBack(seconds) {
2 const currentTime = await player.getCurrentTime();
3 player.setCurrentTime(Math.max(0, (currentTime - seconds)));
4}
5 
6export default {
7 methods: {
8 async goBack(seconds) {
9 const currentTime = await player.getCurrentTime();
10 player.setCurrentTime(Math.max(0, (currentTime - seconds)));
11 }
12 }
13}

One last thing I want to improve is those two axios requests that update the video progress. We can remove that duplication by extracting a new updateProgress(percent) function, and inline the vimeo event listeners.

1onMounted(() => {
2 player = new Player(useSlots().default()[0].el);
3 player.on('play', () => player.setPlaybackRate(currentSpeed.value));
4 
5 if (props.trackProgress) {
6 player.on('progress', throttle((event) => {
7 axios.put(`/lessons/${props.lessonId}/progress`, {percent: event.percent});
8 }, 10000));
9 player.on('progress', throttle((event) => updateProgress(event.percent), 10000));
10 
11 
12 player.on('ended', () => {
13 axios.put(`/lessons/${props.lessonId}/progress`, {percent: 1});
14 });
15 player.on('ended', () => updateProgress(1));
16 }
17});
18 
19function updateProgress(percent) {
20 return axios.put(`/lessons/${props.lessonId}/progress`, {percent: percent});
21}

That was it. We’ve successfully converted a vue component written using the options API to the new vue 3 script setup syntax and composition API.

Make sure to check the Code tab at the start of the page to see the final result!