From options API to script setup using the composition API
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>
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!
-
1Managing SVG icons04:57
-
2Programmatically focus elements to provide a better user experience08:29
-
3Building a CopyToClipboard renderless component in VueJS08:29
-
4Create reusable form components using Vuejs 3 multiple v-models06:58
-
5Vue 3: ref and reactive05:34
-
6Loading scripts inside Vue components08:14
-
7Google maps autocomplete with VueJS10:52
-
From options API to script setup using the composition API07:04
-
9VueJS loading button with TailwindCSS06:48
-
10Component communication in Vue3: sibling, parent, and child components06:50
-
11Building a Vue JS textarea autoresize component05:13
-
12Vue watch multiple values at once03:55
-
13Building a Vue tooltip component using TailwindCSS and Floating UI16:27
-
14Vue templates: avoid functions, use computed properties instead05:12
-
15Build an Alert component using Vue, TailwindCSS and CVA17:23
-
16When to pass function props in vue 302:35
-
17Reusable avatar component11:23
-
18Table of Contents component using VueJS13:43
-
19Cleaner form fields in VueJS14:54
-
20Button component12:16
-
21Vue async combobox component19:14
-
22Vue State and LocalStorage: Perfect Sync Made Simple!07:44
-
23Nuxt 3 environment variables config08:36
-
24Streamline Vue Forms with this useForm composable07:36
-
1Managing SVG icons04:57
-
2Programmatically focus elements to provide a better user experience08:29
-
3Building a CopyToClipboard renderless component in VueJS08:29
-
4Create reusable form components using Vuejs 3 multiple v-models06:58
-
5Vue 3: ref and reactive05:34
-
6Loading scripts inside Vue components08:14
-
7Google maps autocomplete with VueJS10:52
-
From options API to script setup using the composition API07:04
-
9VueJS loading button with TailwindCSS06:48
-
10Component communication in Vue3: sibling, parent, and child components06:50
-
11Building a Vue JS textarea autoresize component05:13
-
12Vue watch multiple values at once03:55
-
13Building a Vue tooltip component using TailwindCSS and Floating UI16:27
-
14Vue templates: avoid functions, use computed properties instead05:12
-
15Build an Alert component using Vue, TailwindCSS and CVA17:23
-
16When to pass function props in vue 302:35
-
17Reusable avatar component11:23
-
18Table of Contents component using VueJS13:43
-
19Cleaner form fields in VueJS14:54
-
20Button component12:16
-
21Vue async combobox component19:14
-
22Vue State and LocalStorage: Perfect Sync Made Simple!07:44
-
23Nuxt 3 environment variables config08:36
-
24Streamline Vue Forms with this useForm composable07:36