Vue functions API RFC, compared with React hooks

The Vue core team recently released a proposal for a “functions API,” essentially implementing React hooks in Vue.

The functions API shares a lot of the advantages of hooks: Compared with class components, stateful logic is more easily extractable and composable. Compared to HOCs, there are no unnecessary components created and prop naming clashes are better avoided.

Here is a side-by-side for a simple program:

Counting:

Vue:

<template>
  <span>{{ count }}</span>
</template>

<script>
  import { value } from "vue";

  export default {
    setup() {
      const count = value(0);
      const increment = () => {
        count.value++;
      };
      return {
        count,
        increment
      };
    }
  };
</script>

React:

import React, { useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)} />
    </div>
  );
}

Comparison with Hooks

A few differences help to give it a bit more of the feel of vanilla Javascript, rather than a framework.

One is the separation of logic and the view via the setup function. Where hooks muddy things a bit by putting a value that only needs to be created once (setCount) inside a function that’s called on every render (App), Vue makes it clear that increment is only being defined once, in setup. Vue’s optional template system - you can define your view in JSX, too.

The setup function also eliminates the need for the useCallback hook. It’s normally used to prevent unnecessary re-renders because of repeatedly recreating callback functions, but not needed if those are defined once.

Vue’s value function is just a wrapper, so you can just update the wrapped value directly:

const count = value(0);
count.value++;

While this seems to discourage updating values in a functional way, it is possible to create purely functional updaters through computed:

const count
const writableComputed = computed(
  // read
  () => count.value + 1,
)

There’s also a dependency injection API that’s similar to the React context API:

import { provide, inject } from "vue";

const CountSymbol = Symbol();

const Ancestor = {
  setup() {
    // providing a value can make it reactive
    const count = value(0);
    provide({
      [CountSymbol]: count
    });
  }
};

const Descendent = {
  setup() {
    const count = inject(CountSymbol);
    return {
      count
    };
  }
};

The watch function is similar to useEffect, but more explicitly reactive: it subscribes to changes in either props or state, rather than having to implement that check yourself.

Conclusions

The reactions to the proposal are mostly negative, mainly saying that it ruins Vue’s simplicity, or that it’s trying to beat React at its own game. It’s true, for instance, that this example, with the same component implemented with the function api and the current Vue api, doesn’t show a clear winner: The component options in the original separates code along one dimension (methods, data, lifecycle), but the function api does along another (useFetch, useMouse). But it’s easy to see how the function API could have advantages in a larger codebase for composing stateful effects.

The question remains whether the functions API is worth using over React hooks. While functions appears like a pure improvement, it’s perhaps not worth abandoning the superior support and ecosystem of React over relatively minor differences.