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