Introduction to Vue.js composition API

Hello!

Abdelrahman Awad

Software Engineer

Open-source contributor and creator of vee-validate

Composition API

A set of additive, function-based APIs that allow flexible composition of component logic.

Component Logic Parts

Any component logic might include some or all of the following:

  • State (Data, Computed) properties.
  • Lifecycle Methods (mounted, created, etc...)
  • Watchers.
  • Methods.
  • Selectors and References to elements.

The problem

What are we trying to solve?

function makeHttpCall(endpoint) {
  // some logic...
}

Example: Assume we want to add the ability to make HTTP calls to our components

Solution #1

Extending the Prototype

Vue.prototype.$http = makeHttpCall;

// later on in our component
this.$http('/users');

Extending the prototype

Pros:

  • Great for function sharing.
  • Very easy to setup.

Cons:

  • stateless logic, like "isFetching" boolean state.
  • Unclear sources for shared functions.
  • Hard to organize (it's a 1-liner).
  • Hard to maintain, again because its a 1-liner.

Solution #2

Mixins

Vue.mixin({
  data: () => ({
    isFetching: false,
    response: null
  }),
  methods: {
    makeHttp (endpoint) {
      // some logic to fetch and set is fetching.
    }
  }
});

// Later on in our components:
this.makeHttp('/api/users');

Mixins

Pros:

  • Full component API availability.
  • Easy to setup and organize.

Cons:

  • Unclear property/method sources.
  • Namespace clashing and conflicts.

Solution #2

Mixins 🩹

We can avoid some of the cons by:

  • Reduce the use of global mixins.
  • Import only the needed mixins per component.
import HttpMixin from '@/mixins/http';

export default {
  mixins: [HttpMixin],
  // ... other options
};

Solution #3

Higher-order Components

A higher order function is a function that takes a function as an argument, or returns a function

A higher-order component is a function/component that takes a component as an argument and returns a new component with augmented behavior.

Higher-order Components

// Base components
const ListView = {
  props: ['items'],
  template: `
    <ul>
      <li v-for="item in items">{{ item }}</item>
    </ul>
`
};


const SelectInput = {
  props: ['items'],
  template: `
    <select>
      <option value="">None</option>
      <option v-for="item in items" :value="item">{{ item }}</option>
    </select>
`
};


Higher-order Components

// Higher-order-component function
function withData (component) {
  return {
    props: ['url'],
    data: () => ({
      items: []
    }),
    created () {
      fetch(this.url).then(res => res.json()).then(data => {
        this.items = data;
      });
    },
    render (h) {
      return h(component, { props: { items: this.items } });
    }
  }
};

Higher-order Components

const ListViewWithData = withData(ListView);
const SelectInputWithData = withData(SelectInput);

export default {
  components: {
    SelectInputWithData,
    ListViewWithData
  },
  template: `
    <div>
      <ListViewWithData url="/api/users"></ListViewWithData>
      <SelectInputWithData url="/api/users"></SelectInputWithData>
    </div>
  `
};

Higher-order Components

Higher-order Components

Pros:

  • Easy to follow declarative API.
  • Easy to organize.                                                        

Cons:

  • Performance overhead caused by initializing extra components.
  • It can require a lot of setup.
  • Prone to "Pyramid of doom"
  • Non-standard API, has different styles and approaches.

Solution #4

Renderless Components (scoped slots)

A type of higher-order component that utilizes the slot to render its content, exposing logic to the slot via slot props.

<DataProvider v-slot="{ isFetching, response }">
 <!-- Some child Nodes --> 
</DataProvider>

Renderless Components (scoped slots)

const DataProvider = {
  props: ['url'],
  data: () => ({
    response: null,
    isFetching: false
  }),
  created () {
    // fetch the data.
  },
  render () {
    const slot = this.$scopedSlots.default({
      response: this.response,
      isFetching: this.isFetching
    });
    
    return Array.isArray(slot) ? slot[0] : slot;
  }
};

Renderless Components (scoped slots)

<DataProvider url="/api/users" v-slot="{ response }">
  <ListView :items="response"></ListView>
</DataProvider>

<DataProvider url="/api/users" v-slot="{ response }">
  <select>
    <option>None</option>
    <option v-for="item in response">{{ item }}</option>
  </select>
</DataProvider>

Renderless Components (scoped slots)

Renderless Components (scoped slots)

Pros:

  • Easy to follow declarative API.
  • Easy to organize.
  • Flexible.
  • Standard documented API.                                      

Cons:

  • Performance overhead caused by initializing extra components.
  • Prone to "Pyramid of doom"

Vue Composition API

The composition API for Vue 3.0 introduces function-based API to build and compose logic using Vue.js core capabilities without the need to wrap them into components.

Exposed Capabilities

  • Reactive values via ref() and reactive().
  • Computed values via computed().
  • Watching reactivity via watch().
  • Lifecycle Hooks: onMounted, onCreated, ... etc

Primer Example

import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  // initial reactive values.
  const x = ref(0)
  const y = ref(0)

  // update values
  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}

API Breakdown: setup

export default {
  setup() { // ...
  }
};

The setup() function primarily serves as an entry point where all the composition functions are invoked.

Only called once!

API Breakdown: ref

import { ref } from 'vue'

const x = ref(0);

Initialization

x = 10; // wrong!

x.value = 10; // ✔

Mutation

👈 This is called value wrapping!

API Breakdown: Lifecycle Events

import { onMounted, onCreated, onUnmounted } from 'vue';

onCreated(() => {
  console.log("I'm being created");
});

onMounted(() => {
  console.log("I'm being mounted");
});

onUnmounted(() => {
  console.log("I'm being unmounted");
});

API Breakdown: Watch

import { ref, watch } from 'vue';

const x = ref(0);

watch(() => {
  console.log('x has changed!', x.value);
});

Example: Http Composition Function

Example: Http Composition Function

Why is this the best solution (yet!)

Allows us to better build and isolate behavior without thinking of extra logic like rendering.

Little to no need to use "this" anymore, allowing for "purer" code.

The code can be organized by logical concerns rather than options type.

Comparison with React Hooks

Unlike React hooks, the setup() function is called only once. 

  • Not sensitive to call order and can be conditional;
  • Not called repeatedly on each render and produces less GC pressure;
  • Not subject to the issue where useEffect and useMemo may capture stale variables if the user forgets to pass the correct dependency array.

Thank You

Introduction to Vue.js Composition API

By Abdelrahman Awad

Introduction to Vue.js Composition API

  • 1,127