Composing Masterpieces
with the Vue 3 Composition API
The Problem
We want a clean way to reuse logic between components and separate concerns.
This component handles both fetching data and computing values—it'd be excellent if it could focus only on displaying the data.
export default {
props: {
city: { type: String, required: true }
},
components: { ... },
data() {
return {
forecast: []
}
},
computed: {
averageTemperature() { ... }
},
methods: {
fetchForecast() { ... }
},
watch: {
city: 'fetchForecast'
},
mounted() {
this.fetchForecast()
}
}
Separation of Focus
As our component grows larger, it's hard to track down all of the pieces that work together.
The Composition API allows us to create small files with all of the logic in one place.
export default {
props: {
city: { type: String, required: true }
},
components: { ... },
data() {
return {
forecast: []
}
},
computed: {
averageTemperature() { ... }
},
methods: {
fetchForecast() { ... }
},
watch: {
city: 'fetchForecast'
},
mounted() {
this.fetchForecast()
}
}
The Vue 2 Approach
In Vue 2, there are two major ways to try to solve this problem—writing many small components and using mixins.
The setup
option
The setup
option is where all of the magic happens. setup
is run before the component is created and can provide values for the component to use.
export default {
setup(props) {
alert(props.city)
return {
...
}
}
}
this
Because setup
is executed before the component is created, there is no access to this
, nor is there any access to the component's data, methods, or computed values.
Not having access to values that exist on a component's instance is actually a great thing—all of the logic for a composable is self-contained.
this
Fetching data in setup
import { fetchForecast } from './api'
export default {
setup(props) {
const forecast = []
const getForecast = async () => {
forecast = await fetchForecast(props.city)
}
return {
forecast,
getForecast
}
}
}
A crash course on Vue 3's reactivity API
Vue 3 ships with a fully standalone reactivity API, which is useful when working with the composition API.
Formerly known as Vue.observable, this function converts any object into a reactive one.
reactive
import { reactive } from 'vue'
reactive({
name: 'Bob Smith',
age: 35
})
reactive
ref
Refs make standalone values reactive.
When used in Vue templates, there's no need to access .value
.
import { ref } from 'vue'
const lightsOn = ref(true)
console.log(lightsOn.value) // true
lightsOn.value = false
console.log(lightsOn.value) // false
computed
You can now directly make computed values.
const lightsOn = ref(true)
const msg = computed(() => {
if (lightsOn.value) {
return 'The lights are on!'
} else {
return 'The lights are off.'
}
}
console.log(msg.value) // lights on
lightsOn.value = false
console.log(msg.value) // lights off
Fetching data in setup
(with refs!)
import { fetchForecast } from './api'
import { ref } from 'vue'
export default {
setup(props) {
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(props.city)
}
return {
forecast,
getForecast
}
}
}
Using setup
Here we remove forecast
from our data
object and remove the method for getForecast
in favor of our new setup
function.
import { fetchForecast } from './api'
import { ref } from 'vue'
export default {
props: {
city: { type: String, required: true }
},
setup(props) {
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(props.city)
}
return {
forecast,
getForecast
}
},
computed: {
averageTemperature() { ... }
},
watch: {
city: 'getForecast'
},
mounted() {
this.gettForecast()
}
}
setup
props
The props
argument is reactive, but using normal JS destructuring will remove the reactivity of the values. However, Vue provides toRefs
for this purpose.
import { toRefs } from 'vue'
export default {
setup(props) {
let { city } = toRefs(props)
return {
...
}
}
}
setup
context
The setup
function accepts a second argument called context
.
There are three properties that can be accessed on context
:
context.attrs
context.slots
context.emit
export default {
setup(props, { attrs, slots, emit }) {
// attrs and slots are stateful objects
// so don't destructure them!
return {
...
}
}
}
Lifecycle hooks
Vue provides us with access to all of the available lifecycle hooks to register functions directly within setup
.
The names of these functions are the same as when using them in a component normally, but with an "on" prefix.
https://v3.vuejs.org/guide/composition-api-lifecycle-hooks.html
Using onMounted
Now we're able to remove our call to getForecast
in mounted
to just popping it into our setup
function.
import { fetchForecast } from './api'
import { ref, onMounted } from 'vue'
export default {
props: {
city: { type: String, required: true }
},
setup(props) {
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(props.city)
}
onMounted(getForecast)
return {
forecast,
getForecast
}
},
computed: {
averageTemperature() { ... }
},
watch: {
city: 'getForecast'
}
}
watch
const counter = reactive({ num: 0 })
watch(() => counter.num, (num, oldNum) => {
// do your thing, homie
})
watch
allows us to explicitly declare when an effect should run, via functions or refs directly.
const counter = ref(0)
watch(counter, (num, oldNum) => {
// do what you gotta do
})
Applying watch
With watch
, we're able to ensure that our component re-fetches the weather data when the city is changed, all from within setup
.
import { fetchForecast } from './api'
import { ref, toRefs, watch, onMounted } from 'vue'
export default {
props: {
city: { type: String, required: true }
},
setup(props) {
const { city } = toRefs(props)
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(props.city)
}
onMounted(getForecast)
watch(city, getForecast)
return {
forecast,
getForecast
}
},
computed: {
averageTemperature() { ... }
}
}
Applying computed
With computed
, we're able to provide averageTemperature
directly from setup
.
const averageTemperature = computed(() => {
const totalTemp = forecast.value.reduce((acc, { temp }) => acc + temp, 0)
return totalTemp / forecast.value.length
})
Our component thus far
import { fetchForecast } from './api'
import { ref, toRefs, watch, onMounted, computed } from 'vue'
export default {
props: {
city: { type: String, required: true }
},
setup(props) {
const { city } = toRefs(props)
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(props.city)
}
onMounted(getForecast)
watch(city, getForecast)
const averageTemperature = computed(() => {
const totalTemp = forecast.value.reduce((acc, { temp }) => acc + temp, 0)
return totalTemp / forecast.value.length
})
return {
forecast,
getForecast,
averageTemperature
}
}
}
Composition Functions
The real power of the composition API is the ability to compose many small pieces together. This enables us to reuse exactly the pieces we need in various components, while still having access to the full power of everything we've written.
useForecast
// useForecast.js
import { fetchForecast } from './api'
import { ref, onMounted, watch } from 'vue'
export default function useForecast(city) {
const forecast = ref([])
const getForecast = async () => {
forecast.value = await fetchForecast(city.value)
}
onMounted(getForecast)
watch(city, getForecast)
return {
forecast,
getForecast
}
}
useAverageTemperature
// useAverageTemperature.js
import { computed } from 'vue'
export default function useAverageTemperature(forecast) {
const averageTemperature = computed(() => {
const totalTemp = forecast.value.reduce((acc, { temp }) => acc + temp, 0)
return totalTemp / forecast.value.length
})
return {
averageTemperature
}
}
Composing it all
import { toRefs } from 'vue'
import useForecast from './useForecast'
import useAverageTemperature from './useAverageTemperature'
export default {
props: {
city: { type: String, required: true }
},
setup(props) {
const { city } = toRefs(props)
const { forecast, getForecast } = useForecast(city)
const { averageTemperature } = useAverageTemperature(forecast)
return {
forecast,
getForecast,
weeklyAverage: averageTemperature
}
}
}
Provide/Inject
// Main.vue
import Child from './Child'
export default {
template: `<div><Child /></div>`,
components: {
Child
},
provide {
city: 'Boston, MA'
}
}
// Child.vue
export default {
inject: ['city']
}
Provide/Inject with Composition
// Main.vue
import { provide } from 'vue'
import Child from './Child'
export default {
template: `<div><Child /></div>`,
components: {
Child
},
setup() {
provide('city', 'Boston, MA')
}
}
// Child.vue
import { inject } from 'vue'
export default {
setup() {
const city = inject('city')
return {
city
}
}
}
Provide/Inject with Reactivity
// Main.vue
import { provide, ref } from 'vue'
import Child from './Child'
export default {
template: `<div><Child /></div>`,
components: {
Child
},
setup() {
const city = ref('Boston, MA')
provide('city', city)
}
}
// Child.vue
import { inject } from 'vue'
export default {
setup() {
const city = inject('city')
return {
city
}
}
}
Updating Provided Values
// Main.vue
import { provide, ref } from 'vue'
import Child from './Child'
export default {
template: `<div><Child /></div>`,
components: {
Child
},
setup() {
const city = ref('Boston, MA')
const setCity = (newCity) => {
city.value = newCity
}
provide('city', city)
provide('setCity', setCity)
}
}
// Child.vue
import { inject } from 'vue'
export default {
setup() {
const city = inject('city')
const setCity = inject('setCity')
return {
city,
setCity
}
}
}
Preventing Child Mutations
// Main.vue
import { provide, ref, readonly } from 'vue'
import Child from './Child'
export default {
template: `<div><Child /></div>`,
components: {
Child
},
setup() {
const city = ref('Boston, MA')
const setCity = (newCity) => {
city.value = newCity
}
provide('city', readonly(city))
provide('setCity', readonly(setCity))
}
}
Composition API Guidelines
- Make small composition functions to handle business logic
- Use the reactivity API to make things reactive
- Use multiple composition functions in your components for the logic it needs
- Let your components focus on displaying data
Oscar Spencer
Software Engineer @
Twitter: @oscar_spen
GitHub: ospencer
grain-lang.org
Composing Masterpieces with the Vue 3 Composition API
By Oscar Spencer
Composing Masterpieces with the Vue 3 Composition API
- 132