Vue Composition API vs React Hooks Quick Comparison

February 22nd 2020

Today we’ll be comparing two relatively new concepts in Vue and React. Vue recently released the Vue Composition API RFC which is an additive API that changes the setup of Vue components to make use of reactivity in Vue and to help developers organize their components by logical concern to avoid bloated components. Since React 16.8, React has introduced React Hooks API which is similar to Vue in that it taps into the reactive system of the React library, but also allows you to use Stateful, functional component – or in other words, you can use state in a functional component. Previously to React 16.8 you could only monitor state (and other features in React) via React Class Components. Hooks bring state management to functional components! Both Vue Composition API and React Hooks API have some similarities and also few differences and we will explore both APIs in this article.

Let’s start with the Vue Composition API with some simple examples to explore the new features it brings to the Vue library. We will first need to create a Vue project and our tooling of choice do do this is the Vue Cli. We can get started using the vue cli by installing it globally with our package manager of choice:

npm install -g @vue/cli
# OR
yarn global add @vue/cli

Double check it was installed: vue --version and you should see the Vue Cli version that has been installed. With the global vue cli installed we can scaffold a vue project quickly by running: vue create vue-composition-api and wee will be prompted to pick a preset. You can choose the default preset which comes with a basic Babel + ESLint setup. Since there hasn’t been a production release of Vue 3 which includes the composition api we will need to install the package that allows use to use the vue-composition-api in a Vue 2 project. We can go ahead and cd into our project folder: cd vue-composition-api and install the official package:

npm install @vue/composition-api --save
// OR
yarn add @vue/composition-api

Next we must install @vue/composition-api via Vue.use() before using the APIs methods it provides. Let’s open up our src/main.js file and add:

import VueCompositionApi from '@vue/composition-api';

Vue.use(VueCompositionApi);

This allows us to import the api functionality into our components. For a basic example we will create a Counter.vue component that utilizes both { reactive, computed } methods. Let’s create our component: touch src/components/Counter.vue and then we will want to open our src/App.vue file and include our Counter.vue component. Our App.vue file should look like:

<template>
  <div id="app">
    <Counter />
  </div>
</template>

<script>
import Counter from '@/components/Counter'

export default {
  name: 'app',
  components: {
    Counter
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Now we can head back over to our Counter.vue file and start using our vue-composition-api. First we need to import some methods to use the api. In our script tag let’s import: reactive, computed and toRefs. We will explain all of these later but for now we can define:

<script>
  import { reactive, computed, toRefs } from '@vue/composition-api'
  ...
</script>

Now we can create our setup method. If you are familiar with classes and class instantiation, this method is similar to a constructor method for classes. The setup method controls the logic of the component. It receives props and context as arguments and returns an object that can be used in our <template> tag for reactive data properties. Let’s define our setup method

<template>
  <div>
    Count is: {{ count }} and if we double it: {{ double }}
  </div>
</template>
<script>
  import { reactive, computed, toRefs } from '@vue/composition-api'
  export default{
    setup(){
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
      })

      return toRefs(state)
    }
  }
</script>

There seems to be a lot going on here. Let’s break it down. We imported the reactive method from the composition api and we’re using it to set a state object. Wrapping the state object in reactive is similar to wrapping our reactive data in the data() method for Vue 2:

export default{
  data(){
    return {
      count: 0
    }
  }
}

The reactive method makes our state object (and properties) reactive in the template/DOM. We are also importing the computed method and we’re using it to create a computed property of our state object. This is similar to the Vue 2 computed property:

export default{
  data(){
    return {
      count: 0
    }
  },
  computed: {
    double: () => this.count * 2
  }
}

Once we get into React Hooks we will see how we can use the useEffect hook in a similar way to update a state property called double. Finally, we are using the toRefs method. What this method does is it return the object that it receives (in our case the state object with all of the properties destructured and ready for use in our template, otherwise your component might look like:

<template>
  <div>
    Count is: {{ state.count }} and if we double it: {{ state.double }}
  </div>
</template>
<script>
  import { reactive, computed, toRefs } from '@vue/composition-api'
  export default{
    setup(){
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
      })

      return { state }
    }
  }
</script>

As you can see the toRefs() allows the variable interpolation in the template tags to be consistent with how we would right it in Vue 2. Since a big benefit of the Vue Composition API is separation of concerns inside our components we can also define methods that are specific to our reactive state within the setup() hook. Let’s take a look at what that might look like by defining a function that increments our state.count.

<template>
  <div>
    <button @click="increment">Increment Counter</button>
    <span :style="{ marginLeft: '1em' }">Count is: {{ count }} and if we double it: {{ double }}</span>
  </div>
</template>
<script>
import { reactive, computed, toRefs } from '@vue/composition-api'
export default{
  name: 'counter',
  setup(){
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })

    const increment = () => {
      state.count++
    }

    return {...toRefs(state), increment}
  }
}
</script>

A couple notes of difference is our return object in our setup method now has an additional property called increment (which is our function to increment the state.count), because of this extra property we have to spread our toRefs() returned object. Our increment method, increments our state.count. This is different from React in that React does not mutate state whereas, in Vue the state object is mutated. Last, we have access to our increment method in our template and we v-on:click (using the shorthand @click directive to invoke the increment method on click. One benefit of the Composition API is that we can reuse logic across components. For example, we can refactor our Counter.vue component to allow access to the functionality that we’ve created. Let’s take a quick look at refactoring:

<template>
  <div>
    <button @click="increment">Increment Counter</button>
    <span :style="{ marginLeft: '1em' }">Count is: {{ count }} and if we double it: {{ double }}</span>
  </div>
</template>
<script>
import { reactive, computed, toRefs } from '@vue/composition-api'
export const counter = () => {
  const state = {
    count: 0,
    double: computed(() => state.count * 2)
  }

  const increment = () => {
    state.count++
  }

  return { state, increment}
}
export default{
  name: 'counter',
  setup(){
    const { state, increment } = counter()
    return { ...toRefs(reactive(state)), increment }
  }
}
</script>

This appears a little strange at first, but now we are able to reuse the functionality for our counter by exporting it as a const and now we can import that functionality into another component just like this:

<template>
  <div>
    <button @click="increment">Increment Counter</button>
    <button @click="decrement">Decrement Counter</button>
    <span :style="{ marginLeft: '1em' }">Count is: {{ count }} and if we double it: {{ double }}</span>
  </div>
</template>
<script>
import { toRefs, reactive,  computed } from '@vue/composition-api'
import { counter } from '@/components/Counter'

export default{
  name: 'counter',
  setup(){
    const { state, increment } = counter()
    const decrement = () => {
      state.count-- 
    }
    return { ...toRefs(reactive(state)), increment, decrement }  
  }
}
</script>

By doing so we’re able to extend the functionality of our Counter.vue component. How cool is that?

Comparison to React Hooks

The motivation behind React hooks is similar to that of the Vue Composition API. One key similarity is extraction of stateful logic from components:

With Hooks, you can extract stateful logic from a component so it can be tested independently and reused.

https://reactjs.org/docs

Secondly, stateful class components require a strong understanding of the this keyword and context scope in Javascript which is different then a lot of other languages.

In addition to making code reuse and code organization more difficult, we’ve found that classes can be a large barrier to learning React. You have to understand how this works in JavaScript, which is very different from how it works in most languages. You have to remember to bind the event handlers.

https://reactjs.org/docs

Vue has similar challenges in Vue 2 due to the fact that it relies on a single this context for exposing component data, properties and methods:

Vue’s current API has posed some challenges when it comes to integration with TypeScript, mostly due to the fact that Vue relies on a single this context for exposing properties, and that the use of this in a Vue component is a bit more magical than plain JavaScript

Vue Composition API RFC

Let’s take a similar look at how we would write our counter component in React. We can use Create React App (CRA) to scaffold a new react project. We can scaffold the project by typing npx create-react-app react-hooks which will create a new react project. We can then change into our react-hooks folder and start the development server: cd react-hooks && yarn start

We can also make a components folder inside our project folder to hold our counter component: mkdir src/components and then create a Counter.js file vim src/components/Counter.js inside our file we can start declaring our component:

import React, {useState} from 'react'
  
const Counter = (props) => {
  const [count, setCount] = useState(0)

  const increment = () => {
    setCount(count + 1)
  }

  const double = useMemo(() => count * 2, [count])

  return(
    <div>
      <button onClick={increment}>Increment</button>
      <span style={{ marginLeft: '1em' }}>Count is: { count } and if we double it: { double }</span>
    </div>
  )
}

export default Counter

You can see there are some similarities here between how Vue handles components in the Composition API and how React handles it’s components. We are using the hook useState to make use of reactivity that it returns as well as the helper function it returns setCount. Unlike Vue, React does not mutate the state directly but uses the setCount function to pass a new value count + 1 to set the count state. What if we wanted refactor to use this in another function? We would do this by creating a Custom React Hook and using it in our component. Let’s refactor and then walk through what we’re doing:

import React, {useState, useEffect} from 'react'
  
export const useCounter = () => {
  const [state, setState] = useState({
    count: 0
  })
  
  useEffect(() => {
    const double = state.count * 2
    setState({ ...state, double })
  }, [state.count])
  
  const increment = () => {
    setState({ ...state, count: state.count + 1 })
  }

  return { state, increment }
}

const Counter = (props) => {
  const { state: { count, double }, increment } = useCounter()

  return(
    <div>
      <button onClick={increment}>Increment</button>
      <span style={{ marginLeft: '1em' }}>Count is: { count } and if we double it: { double }</span>
    </div>
  )
}

export default Counter

To define a custom hook we need the function name to start with ‘use’, so in our component we’re creating a custom hook called useCounter and we are exporting the hook for reuse in other components. We are also changing our count variable and putting it in the state object and updating our state object using setState helper function. We also changed our double computed property to be updated when state.count updates and we’re storing that computed property in our state variable as well. Then we are returning an object with the properties state and increment such as: return { state, increment } so that we can destructure and use these in our other components. Inside our Counter function we are calling the useCounter() hook that we defined and are destructuring our variables for use in our render function.

You can see there are many similarities between the React Hooks and Vue Composition API with each using their own flavor of methods to create reactivity and composition. Hopefully, these newer proposals help in building scalable, high performing apps. Until next time, stay curious, stay creative!