Scraps: Vue 3 & composition API notes
⚖️ Composition vs Options Api
- Vue is moving away from the “Options API” from Vue 2, where concerns and namespacing are spread across
data()
,methods
,watch
,props
, etc. - Vue3 introduces the Composition API, and the concept of a single
setup
that determines what data/functions are returned to the template.
An example of Options API vs Composition API:
Options API
<template>
<div @click="greet()">
Current user: {{username}}
</div>
</template>
<script>
export default {
data() {
return {
username: 'Sarah',
},
},
methods: {
greet() {
console.log(`Hello ${this.username}`);
},
},
mounted() {
this.greet();
}
}
</script>
Composition API
<template>
<div @click="greet()">
Current user: {{username}}
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const username = ref('Sarah');
const greet = () => console.log(`Hello ${username.value}`);
onMounted(() => greet());
return {username, greet};
}
}
</script>
Composition API with <script setup>
<template>
<div @click="greet()">
Current user: {{username}}
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const username = ref('Sarah');
const greet = () => console.log(`Hello ${username.value}`);
onMounted(() => greet());
</script>
Benefits of Composition API
- Can group code by logical concern rather than what it does (eg. data, methods, watchers)
- Can choose which parts of your code to share with your template or make accessible to other components
- Improved ability to cleanly share logic:
- With options API, the only way to share logic was Mixins and Renderless Components
- With composition, it’s easier to reuse logic in multiple components by creating bits of code called composables, or composable functions (more on this further down)
📊 Reactive data
- With the Options API, the
data()
function was intended to be used for both reactive and non-reactive variables - With the Composition API, we explicitly define which variables are reactive
Using ref()
for primitives
Options API
<template>
<div>Hi {{username}} (#{{userId}})</div>
</template>
<script>
export default {
data() {
return {
// if either of these change, we expect the template to rerender
username: 'Sarah',
userId: 1,
},
},
methods: {
setUsername(username) {
this.username = username;
},
}
}
</script>
Composition API
<template>
<div>Hi {{username}} (#{{userId}})</div>
</template>
<script>
import {ref} from 'vue';
export default {
setup() {
// read-only (even if it does change, template will not rerender)
const userId = 1;
// reactive (template will rerender when this changes)
const username = ref('Sarah');
return {
userId,
username,
// 1. methods can just be passed back together with data
// 2. note that we're changing username.value and not username --
// this is because username is wrapped in a Ref
setUsername: (username) => username.value = username,
}
}
}
</script>
Using reactive()
for objects
Options API
<template>
<div>Hi {{user.username}} (#{{user.id}})</div>
</template>
<script>
export default {
data() {
return {
user: {username: 'Sarah', id: 1},
},
},
methods: {
setUsername(username) {
this.user.username = username;
},
},
}
</script>
Composition API
<template>
<div>Hi {{user.username}} (#{{user.id}})</div>
</template>
<script>
import {reactive} from 'vue';
export default {
setup() {
const user = reactive({username: 'Sarah', id: 1});
return {
user,
// note that we do not need "value" as a key here
setUsername: (username) => user.username = username,
};
},
}
</script>
Difference between ref()
vs reactive()
ref()
is usually used for primitives andreactive()
is usually used for objects- Note that
ref()
actually is just a wrapper for areactive()
call - Destructuring a
reactive()
object will make the individual fields non-reactive; in this case, you’ll want to turn each of the reactive children into refs using thetoRefs()
helper:
import {toRefs, reactive} from 'vue';
//...
setup() {
const user = reactive({name: 'Sarah', id: 1});
return {...toRefs(user)}
}
😵💫 Template refs
Don’t get confused; in Vue 3, template “ref” elements are conceptually the same as the reactive ref()
— you use template “ref” elements by creating a ref()
with the same variable name
<template>
<div>
<input ref="singleInput">
<input ref="multiInput">
<input ref="multiInput">
<input ref="multiInput">
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
// the name must match template ref value; ref elements will always start as "null"
const singleInput = ref(null);
// multiple inputs will give an array
const multiInput = ref([]);
// the ref element doesn't exist until mount!! this can't be called in setup()!
onMounted(() => {
singleInput.value.focus();
})
// must be passed down!!
return { singleInput, multiInput }
}
}
</script>
💻 Computed variables
Options API
<template>
<div>
{{hours}} hours is {{minutes}} minutes
</div>
</template>
<script>
export default {
data() {
return { hours: 1 };
}
computed {
minutes() {
return this.hours * 60;
},
}
}
</script>
Composition API read-only computed
<template>
<div>
{{hours}} hours is {{minutes}} minutes
</div>
</template>
<script>
import {computed} from 'vue';
export default {
setup() {
const hours = ref(1);
const minutes = computed(() => hours.value * 60);
return {hours, minutes};
}
}
</script>
Composition API read-write computed
<script>
import {computed} from 'vue';
export default {
setup() {
const hours = ref(1);
const minutes = computed({
get: () => hours.value * 60,
set: (minutes) => hours.value = minutes / 60,
});
return {hours, minutes};
}
}
</script>
👀 Watchers
Using watch()
Options API
<template>
<div>
Logged in: {{ username }}
</div>
</template>
<script>
export default {
data() {
return { username: 'Sarah' };
},
watch: {
username(newVal, oldVal) {
console.log(`User change: ${oldVal} -> ${newVal}`);
}
},
}
</script>
Composition API
<template>
<div>
Logged in: {{ username }}
</div>
</template>
<script>
import {watch} from 'vue';
export default {
setup() {
const username = ref('Sarah');
// first arg: (1) ref/reactive OR (2) getter fn OR (3) array of ref/reactive/getter
watch(username, (newVal, oldVal) => console.log(`User change: ${oldVal} -> ${newVal}`));
return {username};
}
}
</script>
Composition API watching multiple refs
setup() {
const username = ref('Sarah');
const userId = ref(1);
watch(
[username, userId],
([newUsername, oldUsername], [newUserId, oldUserId]) => {/* ... */}
);
return {username, userId};
}
Using watchEffect()
- An alternative to
watch()
setup() {
const username = ref('Sarah');
const userId = ref(1);
// this triggers whenever username or userId change
watchEffect(() => console.log(`Logged in user: ${username.value}, id: ${userId.value}`)));
return {username, userId};
}
Difference between watchEffect()
and watch()
watchEffect()
:- Will automatically watch any ref/reactive/getter dependencies
- Will ALWAYS trigger immediately (and then on every change thereafter)
- Does not return the old value of the ref/reactive/getter dependencies
watch()
:- Lets you explicitly specify which ref/reactive/getters the function is dependent on
- By DEFAULT will not trigger immediately (will only trigger on changes), but can be configured to trigger immediately also
- Does return both the new and old value of the ref/reactive/getter dependencies
- Seems more configurable & flexible
♻️ Lifecycle hooks
Options API
<script>
export default {
mounted() {
console.log('mounted');
},
}
</script>
Composition API
<script>
import {onMounted} from 'vue';
export default {
setup() {
onMounted(() => console.log('mounted'))
}
}
</script>
Nothing surprising here! Can read more about available hooks in the Vue JS documentation:
🍬 <script setup>
- imo this is very ugly but vue strongly recommends use of script setup
<script setup>
lets you cut out thesetup()
function —- everything inside the
<script setup></script>
tags will be treated as if it’s inside thesetup()
function - all top-level variables are exposed to the template
- everything inside the
- idk if there’s a reason but every example i’ve seen that uses
<script setup>
also puts the script above the template instead of below
<script setup>
import { ref } from 'vue'
const greeting = 'Hello world!';
const count = ref(0);
function alert() {
console.log(greeting)
}
</script>
<template>
<div>
<a href="#" @click="alert">{{greeting}}</a>
<span @click="count++">{{count}}</span>
</div>
</template>
Warnings
- Remember that
<script setup>
will become yoursetup()
— accordingly it’s not safe to put top-level code here that you would normally put outside of yoursetup()
definition — this code will be run every single time the component gets setup/initialized
📝 Props, components, etc
These are mostly unchanged!
Options API
<template>
<my-component>{{myProp}}</my-component>
</template>
<script>
import MyComponent from './components/MyComponent';
export default {
components: {MyComponent},
props: ['myProp']
}
</script>
Composition API with setup()
<template>
<my-component>{{myProp}}</my-component>
</template>
<script>
import MyComponent from './components/MyComponent';
export default {
components: {MyComponent},
props: ['myProp'],
// props are passed into setup if needed
// but will automatically go down to the template regardless
setup(props) {
console.log(props.myProp);
}
}
</script>
<script setup>
uses defineProps()
<template>
<my-component>{{myProp}}</my-component>
</template>
<script setup>
// this will automatically be exposed to the template
import MyComponent from './components/MyComponent';
import {defineProps} from 'vue';
// use defineProps for importing props
const {myProp} = defineProps(['myProp']);
console.log(myProp);
</script>
📦 Mixins vs Composables
Benefits of composables:
- Can encapsulate data & functionality into a single component (better organization and partitioning of data and functionality; there is no ambiguity about where data or functionality is coming from)
- Can have private data & methods
Mixin
/**********************************************************/
/* CounterMixin.js
/**********************************************************/
export default {
data() {
return { count: 0 };
},
computed: {
countPlusOne() {
return this.count + 1;
},
},
methods: {
increment() {
this.count ++;
}
},
}
/**********************************************************/
/* MyComponent.vue
/**********************************************************/
import CounterMixin from '../mixins/CounterMixin';
import SarahsCounterMixin from '../mixins/SarahsCounterMixin';
import AnotherCounterMixin from '../mixins/AnotherCounterMixin';
export default {
mixins: [CounterMixin, SarahsCounterMixin, AnotherCounterMixin],
created() {
console.log(this.count); // which count is this referring to? ambiguous
console.log(this.countPlusOne); // sussy
this.increment(); // very sussy
},
}
Composable
/**********************************************************/
/* useCounter.js
/**********************************************************/
import {ref, computed, readonly} from 'vue';
// composables can support global data!!
// this variable is initialized outside of the composable function, so every instance of useCounter will share the same count
// move this into the function if you don’t want this behaviour (count will start at 0 for every component)
const count = ref(0);
// use of readonly here means that even though we pass count as a Ref, it can’t be edited without calling increment
export default () => {
const increment = () => count.value ++;
const countPlusOne = computed(() => count.value + 1);
return {count: readonly(count), increment, countPlusOne}
}
/**********************************************************/
/* MyComponent.vue
/**********************************************************/
import {useCounter} from '../composables/useCounter';
import {useSarahsCounter} from '../composables/useSarahsCounter';
import {useAnotherCounter} from '../composables/useAnotherCounter';
export default {
setup() {
const {count, increment, countPlusOne} = useCounter();
const {count: sarahsCount, increment: sarahsIncrement, countPlusOne: sarahsCountPlusOne} = useSarahsCounter();
console.log(count.value);
console.log(countPlusOne.value);
increment();
return {count, countPlusOne, increment};
}
}
🏪 Vuex
<template>
<h1>{{title}}</h1>
</template>
<script setup>
import {useStore} from 'vuex';
import {computed} from 'vue';
const store = useStore(); // useStore() call should be inside setup and not inside getters/computed/hooks etc
const title = computed(() => `Hello world ${store.state.username}`);
const setUsername = (username) => store.commit('setUsername', username);
</script>
🚀 Using SWRV (SWR Vue)
- SWR stands for “stale-while-revalidate”
- Returns any cached (”stale”) data first to the UI
- Send a fetch request for new data (revalidate)
- Returns up to date data after it comes back
- Benefits:
- Makes UI feel faster
- Handles automatic revalidation (when the tab is refocused, on interval, or on reconnect)
- Supports pagination & infinite loading (provides a
useSwrInfinite
hook) - If an error is returned on revalidation, the cached data will remain in cache and be served
No fetcher (default behaviour)
- This will automatically call the API using fetch
<template>
<div>
<div v-if="error">failed to load</div>
<div v-if="!data">loading...</div>
<div v-else>hello {{ data.name }}</div>
</div>
</template>
<script>
import useSWRV from 'swrv'
export default {
name: 'Profile',
setup() {
const { data, error } = useSWRV('/api/user')
return { data, error }
},
}
</script>
Null fetcher
- This will never revalidate and will only cache
- Use case, eg: you have two components which are using the same cache key; if one component is revalidating, you don’t need to revalidate for the second component
setup() {
const { data, error } = useSWRV('/api/user', null)
return { data, error }
}
Explicit fetcher
- Will use the provided Promise for revalidation
- To be honest, I don’t know the use case for this lol
setup() {
const { data, error } = useSWRV('/api/user', () => axios.get('/api/user/'))
return { data, error }
}
Dependent fetching
This is pretty cool...
setup() {
const { data: user } = useSWRV('/api/user')
const { data: projects } = useSWRV(() => user.value && `/api/user/${user.value.id}/projects`)
// the fetcher will not trigger if the cache key is falsy, but will watch `user` for changes since it's reactive
return { user, projects }
},
📔 Reading
- https://vuejs.org/guide/extras/composition-api-faq.html
- https://vuex.vuejs.org/guide/composition-api.html
- https://markus.oberlehner.net/blog/vue-3-composition-api-vs-options-api/
- https://vueschool.io/articles/vuejs-tutorials/what-is-a-vue-js-composable/
- https://vanoneang.github.io/article/v-model-in-vue3.html#recap-the-v-model-directive