loading...

React Hooks API vs Vue Composition API, as explored through useState

vuetraining profile image VueTraining.net ・12 min read

You've read plenty of high-level, abstract articles about React vs Vue, about Hooks API vs Composition API.

This article is different.

This is exploring one specific feature, React's useState hook, and seeing how we can accomplish the equivalent functionality with Vue's Composition API.

While we're exploring, we're going to uncover what I believe are two of the core philosophical differences between React and Vue, and how that effects every single facet of their APIs.

I'm going to offer my opinion on how each framework stacks up, but I'm also going to give you side-by-side code comparisons so you can make your own decision.

This article presumes familiarity with either React or Vue. If you want something for a team completely new to frontend development, sign up for my newsletter and you'll be the first to know when that's ready.

useState and the Click Counter

We're going to start with the basic example shown in the useState documentation. You click a button which adds 1 to a counter.

Code for the click counter app

For those on smaller screens, don't worry - we'll show a larger, copy-pastable version of the code right before we start talking about the mechanics of each component.

Our plan is to spend quite a bit of time dissecting this example - including some design choices that aren't directly related to useState - then tackle a more complicated example with useState to see how both frameworks' solutions bend and break with increased requirements.

Some quick notes before we go farther:

  • I'm relatively new to React, so when possible I'm going to rip code straight from documentation and well-regarded tutorials. If there's a better way of doing things, tell me in the comments.
  • We're "flipping" the script and template tags for Vue's Single File Components, so the code is easier to compare to React. Normally the template tag would go first, then the script tag (and then the style tag, which we've left off of the screenshots)

Okay, those caveats done, let's compare these two pieces of code.

Aesthetics and Readability

Here's the code comparison for the Click Counter again.

Code for the click counter app

The first thing you'll notice is that there are more lines of Vue code, while React has longer individual lines. Personally, I find the React code on this example a bit more aesthetically pleasing because I have a personal vendetta against extra lines and boilerplate code, but the Vue code is a bit easier to digest.

This is especially true with how they've differentiated the pure Javascript portion of the code from template portion of the code. Vue has a script tag and a template tag which clearly differentiate them. In React, they save a few lines by putting it all in one function and asking you to remember that the setup goes in the main body of the function and the template goes in the return value (except when it doesn't).

I think in general, Vue's boilerplate can look bad for very small examples like the following:

// React
const [count, setCount] = useState(0);
// Vue
setup () {
  return {
    count: ref(0)
  }
}

However, if you add a couple more pieces of state, we can see that Vue's code starts looking like the cleaner option.

// React
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(5);
const [count3, setCount3] = useState(27);
// Vue
setup () {
  return {
    count: ref(0),
    count2: ref(5),
    count3: ref(27)
  }
}

And a huge chunk of the boilerplate can be completed by starting your file with the vbase-3 auto-complete in VSCode, so with Vue you'll end up typing about the same number of characters or less.

There's also an RFC in progress for reducing that boilerplate significantly.

Now let's look at useState and ref directly.

useState vs ref

They're not exactly equivalent, but ref (short for "reactive reference") can easily be used to accomplish the same functionality as useState.

Let's first look at how useState is used in React.

useState

Here's the code for the React component.

import { 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>
  );
}

useState is a function that takes one argument, which is the initial state. It returns an array with two values - the initial state, and then a function which can be used to change the state.

You can name the two items in the array anything you want, and you can handle the return array any way you want, but I personally don't see why you'd do anything except the following one-liner that uses array destructuring.

const [foo, setFoo] = useState(initValue)

But if you want to go nuts and name your stuff in a different (worse) manner, React's not going to stop you:

// please don't do this
const myUseStateExample = useState(30)
const foo = myUseStateExample[0]
const aGoodWayToChangeFoo = myUseStateExample[1]

So, presuming we're using the standard naming, we can then use count for the count, and setCount(newValue) to change the value of count.

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

The question I had, as a relative React outsider, is this: why have a variable that can't be changed, and then a function whose sole purpose is to change that variable? Why not just let count = newValue be what sets the count?

The reason, without getting too into the weeds, is that React's component lifecycle and State APIs - which were created before React Hooks and are what the Hooks are "hooking" into - require it. If you just use count = newValue, count won't update properly everywhere it's used because React doesn't know that anything's changed. In general, it appears that you won't have to think about the component lifecycle or the old APIs much while using the Hooks API... unless you're curious enough to dig into why some hooks work the way they do.

ref

Here's the code for the Vue component:

<script>
import { ref } from 'vue'

export default {
  setup () {
    return {
      count: ref(0)
    }
  },
}
</script>

<template>
  <p>You clicked {{count}} times</p>
  <button @click="count++">
    Click me
  </button>
</template>

In Vue, we use ref to create a reactive reference to a value.

setup () {
  return {
    count: ref(0)
  }
}

Now, in our template, we can display and set count, and it will act like a normal Javascript variable.

<p>You clicked {{count}} times</p>
<button @click="count++">
  Click me
</button>

Note the simplicity of this. We can just use count++, or count += 1, or any number of other easy Javascript shortcuts, rather than declaring a new function and feeding a new value into setCount (() ⇒ setCount(count + 1) vs count++). Behind the scenes, Vue turns that one line of Javascript (count++) into an executable function that's run when the click event is fired. Behind the scenes, Vue updates every instance of count when we run count++.

One reason it can do this is because it's using the custom event listener directive @click. The custom syntax allows for simpler code than if we had to use onClick.

I really like the simple code created by Vue's approach. By hiding the layers of complexity, we can get something that's easy to write and easy to read.

But you may be curious about that complexity. How does Vue know to change every instance of count when we run count++? If React has to use setCount, why doesn't Vue have to do something similar?

The short answer is that Vue does do something similar, but they hide it from you so you don't have to think about it - that's one more issue off your plate.

Of course, that complexity-hiding isn't always perfectly seamless. For example, let's look at how we set count within our script block. Now we have to use count.value.

setup () {
  let count = ref(0)
  const addValue = (numAdded) => {
    count.value = count.value + numAdded
  }
  return {
    count,
    addValue
  }
}

The reason is that Vue's reactivity system requires an object in order to function properly, so ref creates an object like {value: 0}. In the template, Vue hides this complexity from you and lets you access and change count without specifically referencing the value property, but in the script block you no longer have this shortcut.

Comparing the code so far

So far, while I personally prefer the Vue code, objectively they're neck and neck.

Both are fairly straightforward once you start playing around with them, with a few slight paper cuts on either side.

React has fewer lines in the setup code, but there's the inconvenient separation between foo and setFoo, and the whole naming foot-gun, that makes the API less easy to use than it could be.

Vue has some great conveniences (compare @click="count++" to onClick={() => setCount(count + 1)}), but slightly more starter boilerplate, and you have to remember to use count.value instead of count while in the script block.

The big difference I see, philosophically, is that

  • React wants their APIs to be "pure functional code" and close to basic Javascript, even if it means foregoing solutions that are more convenient for the developer
  • Vue wants their APIs to be easy to write and read, even if it means custom syntax and slightly more starter boilerplate

Let's see how those philosophical differences inform a more complex case.

Reactive Objects

In this example, we're going to be using a short form that has two inputs - first name and last name.

This particular example was taken from Codevolution's wonderful Hooks series on YouTube, and I then created a Vue version that did the same thing.

Code comparison for form component

First, let's explore the React version

useState and objects

import { useState } from 'react'

function Example() {
  const [name, setName] = useState({first: '', last: ''});

  return (
    <form>
      <input
        type="text"
        value={name.first}
        onChange={e => setName({...name, first: e.target.value})}
      />
      <input
        type="text"
        value={name.last}
        onChange={e => setName({...name, last: e.target.value})}
      />
      <h2>Your first name is - {name.first}</h2>
      <h2>Your last name is - {name.last}</h2>
    </form>
  )
}

Our first line is pretty much the same as last time - we get name and setName from useState, and feed in a default value.

Then when we're displaying the first name and last name, it's also pretty much the same - we use {name.first} and {name.last}.

Where it gets tricky is in the input.

<input
  type="text"
  value={name.first}
  onChange={e => setName({...name, first: e.target.value})}
/>

The input has a value, which is {name.first}. That's straightforward enough.

Then we have onChange. It uses the native onChange event listener, which takes a function. That function has one argument, an event. You can use .target.value on that event to get the input's new value. Then you splat ...name in front of that to turn it into the object that setName wants.

These lines are... fine, I guess. Once you dig into them you can see what everything's doing, and it's using native Javascript and html syntax, so you don't have to learn any new syntax if you already have a firm grasp of those technologies. But there's sure a lot of noise and it's easy to mix things up.
Here's an easy error to make:

<input
  type="text"
  value={name.first}
  onChange={e => setName({first: e.target.value})}
/>

Can you guess what happens with this code?

Well, setName completely replaces the name with whatever argument it's given, so that means that the code above will erase the value of name.last and any other key on the name object. You must remember to splat the old object (setName({...name, key: newValue})) every time.

Here's another easy error to make:

<input
  type="text"
  value={name.first}
/>

Or

<input
  type="text"
  value={name.first}
  onChange={newVal => setName({...name, first: newVal})}
/>

So with all these easy ways to forget something and screw things up, why is it constructed this way?

First, let's look at the Vue solution.

ref and objects

Here's what the Vue code looks like:

<script>
import {ref} from 'vue'

export default {
  setup(){
    return { 
      name: ref({first: '', last: ''})
    }
  }
}
</script>

<template>
  <form>
    <input
      type="text"
      v-model="name.first"
    />
    <input
      type="text"
      v-model="name.last"
    />
    <h2>Your first name is - {{name.first}}</h2>
    <h2>Your last name is - {{name.last}}</h2>
  </form>
</template>

The ref works the same as last time (but remember: if you use it in the script, you have to do name.value.first).

Using it in the template also works the same as last time.

And the input tag is... very simple.

<input
  type="text"
  v-model="name.last"
/>

Honestly, it could just be

<input type="text" v-model="name.last" />

So, so simple.

All you have to know about v-model is that it acts like a "two-way binding". That means that, whenever the input changes, name.last changes. And whenever name.last changes elsewhere, what's shown in the input changes.

But notice I said it "acts like" a two-way binding.

That's because this is just a shortcut for the following code.

<input
  type="text"
  :value="name.first"
  @input="name.first = $event.target.value"
/>

You'll notice some similarities here to the React code. We don't have to do the object splat, and this code is simpler in other ways as well, but we're still having a one-way-bound value and an event (@input) that changes the value based on event.target.value.

Comparing React and Vue on reactive object

This one is a massive win for Vue.

I mean, look at the difference.

// React
<input
  type="text"
  value={name.first}
  onChange={e => setName({...name, first: e.target.value})}
/>
// Vue
<input type="text" v-model="name.first" />

The Vue code is clear as day, while the React code has a bunch of moving parts that, let's be honest, will almost always be set up the exact same way unless the coder makes a mistake.

So why is React like that?

Three reasons.

First, Vue is fine introducing new syntax like v-model or the @input event listener directive. React, on the other hand, wants to use native Javascript and Html as much as possible.

The second is functional purity. setName replaces the entire object, so we have to splat the old name object or we'll end up erasing data. React has made the decision that avoiding mutation is worth the cost of you remembering to do extra steps.

Third, they want to avoid two-way data binding.

Two-way data binding, in addition to being Not Functional, has some surprising effects if you misuse it and have multiple layers of two-way data bindings. It gets harder to track down errors. We learned this the hard way in the early days of Javascript frameworks.

So instead of using two-way data binding, React devs now have to specify one-way data binding and an event. Together they're basically two-way data binding with extra steps, but they're not technically two-way data binding.

Vue says "why have all the extra steps?" and gives you a nice tool to solve the issue. Of course, there are still bad things that can happen with too much two-way data binding, so Vue has some guardrails that prevent you from accidentally making all the old mistakes again. Generally, you'll use v-model with low-level form inputs and a few other places where it's convenient, and have custom event code for other components.

Conclusion

Originally I had several more examples, but they ended up going deep into other parts of React and Vue without shedding much more light on the useState hook... so I'm saving those for my full React vs Vue comparison guide. Now it's time to review what we've learned.

Through exploring some uses of setState, and how to replicate those uses with Vue's Composition API, we've seen pretty clearly a difference of philosophy between React and Vue.

React values pure functional programming and APIs that are clearly recognizable as Javascript functions (except JSX, and that's still pretty clearly a Javascript function once you know how it works). This can be quite intellectually satisfying, especially for someone like me who first learned how to code with a LISP dialect.

Vue values developer ergonomics and making code clear and easy to read, once you learn a couple new pieces of syntax. This can result in fewer bugs and fewer wasted dev-hours.

The question you need to ask yourself when choosing one of these frameworks is - which of those sets of values are more important to you?

What Now?

To get the full React vs Vue guide for free when it's released, sign up for my mailing list.

You can also follow me on twitter. While you're there, follow Lachlan Miller and JS Cheerleader, both of whom shared valuable knowledge that improved this post.

If you're interested in learning Vue, I've created a training course with hands-on learning and guaranteed results.

If you're already using Vue successfully and want assistance, I have consulting availability on a limited basis. Contact me for details.

Posted on by:

vuetraining profile

VueTraining.net

@vuetraining

Learn VueJS with hands-on training and guaranteed results. https://www.vuetraining.net/

Discussion

pic
Editor guide