What does the Vue function API feel like

17 Jun, 2019

Recently a proposal for Vue.js 3.x has been published, and with it a whole new way to use Vue.js via function API that addresses many problems that were present in 2.x API.

I suggest you read the proposal and read the comments there as they explain the rational and the design choices that led to this API before reading my thoughts on this. I will discuss my thoughts without going into details.

What does it look like

In a nutshell, the basic example looks like this:

import { value, computed, watch, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = value(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watch(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}

A few things jump to your mind when seeing this:

  • Where ma data and computed stuff at.
  • What the frick is a setup function.

The new API introduces functions that you will use to compose your functions instead of the component options we used to have in 2.x:

  • value function allows you to define reactive values (references), this is our data replacement.
  • computed function allows you to define computed values based on said reactive references.
  • watch is self explanatory, it watches a reactive reference and executes a callback whenever its value change.
  • onMounted runs a callback whenever the component is being mounted, which is mounted but without shackles.

That's right! data and friends are kinda deprecated, which means we will have to use those functions to create our components with 3.0 onwards.

The setup function is like an entry point for your component.

EDIT UPDATE 26/6/2019 🛑

data and computed and friends are no longer deprecated and the Vue 3.0 API will be compatible with Vue 2. The new API is additive. CALM DOWN 🤚.


What are we trying to solve

This API is designed to solve the largest problem that plagues our web apps: re-using logic across components. We already had a few ways to solve this problem but those solutions fall short in a way or another:

  • mixins: I really don't encourage anyone to use mixins especially if you are going to register them globally with Vue.mixin. They are valid for multiple use-cases and if they solve your problem then don't mind me. But there are some downsides to them, first it is not immediately clear which mixins are registered within a given app or a component, so we could have unused, conflicting units in our component, and aside from that it is not also clear which units are available on our component. I categorize this as a maintenance problem, it may not hurt you but it will hurt whoever comes after you, could as well be you in a few weeks.
  • HoC: Higher order components is a design pattern which - akin to the higher order functions pattern - is defining functions that take a component and returns a new component with modified behavior or presentation. I rarely saw this pattern used in Vue.js community as it is more of a React thing but where ever this pattern might be, there is some cons that are inherit in the pattern itself: props clashing. Assuming you have name prop in the wrapper component, it can easily conflict with the original component that wants that prop as well. Also the matter of performance is highly subjective but I wouldn't worry about that in most cases.
  • slot-scope: Components that are renderless and uses v-slot to provide data to their slot are probably one of the best APIs in Vue.js 2.x and I abuse it whenever I can, it allows you to declaratively share behavior, which is intuitive in my opinion and easily maintainable, but nesting multiple components can be gruesome and when abused you can end up with a pyramid of doom. This is as maintainable as the callback hell.

It becomes apparent that we are trying to re-use logic by using components as an interface for the shared logic, which introduces some complexities and challenges on their own, it would be nice we are able to share logic without worrying about how it will be used in components.


React Hooks to the rescue

Well, sort of. This new API is inspired by the newly hot addition to React API called hooks, and no its not the life-cycle hooks. This is what it looks like:

import React, { useState } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

While very similar, in my opinion it avoids some pitfalls and confusing stuff about the React Hooks API. But I always liked the "feel" of React Hooks, it felt elegant as I was sharing behavior without having to introduce components in one way or another (HoC and v-slot) while also being very clear about which behavior is shared.


It's all about pure logic

Putting aside how different things will be, the new Vue proposal allows us to define reactive data and build computed properties on top of them without having to assign them to components, it is context-free which allows us to better build and isolate behavior without thinking of extra logic like rendering.

Let's put words into examples, assuming we want to share some logic, say a GraphQL query fetching function API. We can start by doing something like this:

import { value } from 'vue';

export const useQuery = (query) => {
  // Define basic query state.
  const data = value(null);
  const errors = value(null);
  const isFetching = value(false);

  // This fn will run the query.
  const execute = async () => {
    isFetching.value = true;
    const response = await apolloClient.query({
      query
    });

    data.value = response.data;
    errors.value = response.errors;
    isFetching.value = false;
  };

  // Return those "units" to be consumed by components.
  return {
    data,
    errors,
    isFetching,
    execute
  };
};

In useQuery we are only focusing on getting the GraphQL query executed while having some useful whistles around it, like being able to tell if the query is in progress or if it failed. This logic would work for virtually any query, and any component can consume this logic like this:

import { useQuery } from './hooks/graphql';
import { onMounted } from 'vue';

const Todos = {
  template: `
    <div>
      <ul v-if="!isFetching">
        <li v-for="todo in data.todos">{{ todo.title }}</li>
      <ul>
    </div>
  `,
  setup() {
    // BAM!
    const { isFetching, execute, data } = useQuery(
      gql`
        {
          todos {
            id
            title
          }
        }
    `);

    // Let's run the query on mounted.
    onMounted(() => {
      execute();
    });

    // Make the data and the progress indicator available to the template.
    return {
      isFetching,
      data
    };
  }
}

What I love about this is we can explicitly specify what is available to the template, how many times did you need a private reactive value on your Vue instance without having to pollute the template with its reference? What about methods then? You had to define your internal methods on the methods key whenever you wanted to do something with this, this is no longer the case and we will be probably referencing this a lot less frequently.


Taking it for a spin

When I finished reading the API, I felt like what we say around here "sitting on an egg" as in I was eagerly dying to tryout the new API and what Ideas might come when playing around with it. But unfortunately, Vue 3.x isn't around until later this year.

So I decided what any reasonable Vue.js developer would do, implement the proposal using Vue.js 2.6+.

I hacked my way until I got a sufficient poc running on Vue.js 2.6+ API. Which is good for charting out your ideas and getting a "feel" for the new API, but don't dare use this in a real app, I'm merely playing around.

So lets see a couple of examples of making use of this API.

useMouse

So here is the useMouse example shown in the RFC examples running:

GraphQL

this is the useQuery example I shown earlier:

If you are interested in the source for the implementation, you can find it here.

EDIT UPDATE 22/6/2019

There is an active project actively implementing the RFC API, use that instead in your projects.


Conclusion

To be honest, I do have some concerns. I discussed this with my teammates and new developers and while some liked it, it does turn the entry-difficulty up a notch or two, Vue has built its popularity by being so easy to use, fast to adopt and quick pick up in production teams with small training costs.

But it will take some discipline to keep things maintainable.

I disagree, in my opinion The maintainability would improve, yes you probably have to do a better job keeping things neat and tidy, but you a much easier way to do that. I have come across many Vue.js projects and maintainability can be thrown out of the window even faster with component options, it took discipline to re-use components in 2.x, and if this API gets approved, we can at least think less about components, and more about re-usable logic and shift our discipline towards elegancy.

Liked it? Subscribe for my latest content!