I created the exact same app in React and Vue. Here are the differences. [2020 Edition]

2020 Edition: Now with React Hooks vs Vue 3 + Composition API!

JavaScript
React
Vue

July 28th, 2020

A few years ago, I decided to try and build a fairly standard To Do App in React and Vue. Both apps were built using the default CLIs (create-react-app for React, and vue-cli for Vue). My aim was to write something that was unbiased and simply provided a snapshot of how you would perform certain tasks with both technologies.

When React Hooks were released, I followed up the original article with a '2019 Edition' which replaced the use of Class Components with Functional Hooks. With the release of Vue version 3 and its Composition API, now is the time to one again update this article with a '2020 Edition'.

Let’s take a quick look at how the two apps look:

React vs Vue. The Immovable Object meets the Irresistible Force!

The CSS code for both apps are exactly the same, but there are differences in where these are located. With that in mind, let’s next have a look at the file structure of both apps:

You’ll see that their structures are similar as well. The key difference so far is that the React app has two CSS files, whereas the Vue app doesn’t have any. The reason for this is because create-react-app creates its default React components with a separate CSS file for its styles, whereas Vue CLI creates single files that contain HTML, CSS, and JavaScript for its default Vue components.

Ultimately, they both achieve the same thing, and there is nothing to say that you can’t go ahead and structure your files differently in React or Vue. It really comes down to personal preference. You will hear plenty of discussion from the dev community over how CSS should be structured, especially with regard to React, as there are a number of CSS-in-JS solutions such as styled-components, and emotion. CSS-in-JS is literally what it sounds like by the way. While these are useful, for now, we will just follow the structure laid out in both CLIs.

But before we go any further, let’s take a quick look at what a typical Vue and React component look like:

A typical React file:

A typical Vue file:

Now that’s out of the way, let’s get into the nitty gritty detail!

How do we mutate data?

But first, what do we even mean by “mutate data”? Sounds a bit technical doesn’t it? It basically just means changing the data that we have stored. So if we wanted to change the value of a person’s name from John to Mark, we would be ‘mutating the data’. So this is where a key difference between React and Vue lies. While Vue essentially creates a data object, where data can freely be updated, React handles this through what is known as a state hook.

Let’s take a look at the set up for both in the images below, then we will explain what is going on after:

React state:

Vue state:

So you can see that we have passed the same data into both, but the structure is a bit different.

With React — or at least since 2019 — we would typically handle state through a series of Hooks. These might look a bit strange at first if you haven’t seen this type of concept before. Basically, it works as follows:

Let’s say we want to create a list of todos. We would likely need to create a variable called list and it would likely take an array of either strings or maybe objects (if say we want to give each todo string an ID and maybe some other things. We would set this up by writing const [list, setList] = useState([]). Here we are using what React calls a Hook — called useState. This basically lets us keep local state within our components.

Also, you may have noticed that we passed in an empty array [] inside of useState(). What we put inside there is what we want list to initially be set to, which in our case, we want to be an empty array. However, you will see from the image above that we passed in some data inside of the array, which ends up being the initialised data for list. Wondering what setList does? There will be more on this later!

In Vue, you would typically place all of your mutable data for a component inside of a setup() function that returns an object with the data and functions you want to expose (which basically just means the things you want to be able to make available for use in your app). You will notice that each piece of state (aka the data we want to be able to mutate) data in our app is wrapped inside of a ref() function. This ref() function is something that we import from Vue and makes it possible for our app to update whenever any of those pieces of data are changed/updated. In short, if you want to make mutable data in Vue, assign a variable to the ref() function and place any default data inside of it.

So how would we reference mutable data in our app?

Well, let’s say that we have some piece of data called name that has been assigned a value of Sunil.

In React, as we have our smaller pieces of state that we created with useState(), it is likely that we would have created something along the lines of const [name, setName] = useState('Sunil'). In our app, we would reference the same piece of data by calling simply calling name. Now the key difference here is that we cannot simply write name = 'John', because React has restrictions in place to prevent this kind of easy, care-free mutation-making. So in React, we would write setName('John'). This is where the setName bit comes into play. Basically, in const [name, setName] = useState('Sunil'), it creates two variables, one which becomes const name = 'Sunil', while the second const setName is assigned a function that enables name to be recreated with a new value.

In Vue, this would be sitting inside of the setup() function and would have been called const name = ref(‘Sunil'). In our app, we would reference this by calling name.value. With Vue, if we want to use the value created inside of a ref() function, we look for .value on the variable rather than simply calling the variable. In other words, if we want the value of a variable that holds state, we look for name.value, not name. If you want to update the value of name, you would do so by updating name.value. For example, let's say that I want to change my name from Sunil to John. I'd do this by writing name.value = "John". I’m not sure how I feel about being called John, but hey ho, things happen! 😅

Effectively React and Vue are doing the same thing here, which is creating data that can be updated. Vue essentially combines its own version of name and setName by default whenever a piece of data wrappeed inside of a ref() function gets updated. React requires that you call setName() with the value inside in order to update state, Vue makes an assumption that you’d want to do this if you were ever trying to update values inside the data object. So Why does React even bother with separating the value from the function, and why is useState() even needed? Essentially, React wants to be able to re-run certain life cycle hooks whenever state changes. In our example, if setName() is called, React will know that some state has changed and can, therefore, run those lifecycle hooks. If you directly mutated state, React would have to do more work to keep track of changes and what lifecycle hooks to run etc.

Now that we have mutations out of the way, let’s get into the nitty, gritty by looking at how we would go about adding new items to both of our To Do Apps.

How do we create new To Do Items?

React:

const createNewToDoItem = () => {
  const newId = generateId();
  const newToDo = { id: newId, text: toDo };
  setList([...list, newToDo]);
  setToDo("");
};

How did React do that?

In React, our input field has an attribute on it called **value. **This value gets automatically updated every time its value changes through what is known as an onChange event listener. The JSX (which is basically a variant of HTML), looks like this:

<input
  type="text"
  placeholder="I need to..."
  value="{toDo}"
  onChange="{handleInput}"
  onKeyPress="{handleKeyPress}"
/>

So every time the value is changed, it updates state. The handleInput function looks like this:

const handleInput = (e) => {
  setToDo(e.target.value);
};

Now, whenever a user presses the **+ **button on the page to add a new item, the **createNewToDoItem **function is triggered. Let’s take a look at that function again to break down what is going on:

const createNewToDoItem = () => {
  const newId = generateId();
  const newToDo = { id: newId, text: toDo };
  setList([...list, newToDo]);
  setToDo("");
};

Essentially the newId function is basically creating a new ID that we will give to our new toDo item. The newToDo variable is an object that takes that has an id key that is given the value from newId. It also has a text key which takes the value from toDo as its value. That is the same toDo that was being updated whenever the input value changed.

We then run out setList function and we pass in an array that includes our entire list as well as the newly created newToDo.

If the ...list, bit seems strange, the three dots at the beginning is something known as a spread operator, which basically passes in all of the values from the list but as separate items, rather than simply passing in an entire array of items as an array. Confused? If so, I highly recommend reading up on spread because it’s great!

Anyway, finally we run setToDo() and pass in an empty string. This is so that our input value is empty, ready for new toDos to be typed in.

Vue:

function createNewToDoItem() {
  const newId = generateId();
  list.value.push({ id: newId, text: todo.value });
  todo.value = "";
}

How did Vue do that?

In Vue, our input field has a handle on it called v-model. This allows us to do something known as two-way binding. Let’s just quickly look at our input field, then we’ll explain what is going on:

<input type="text" placeholder="I need to..." v-model="todo" v-on:keyup.enter="createNewToDoItem" />

V-Model ties the input of this field to a variable we created at the top of our setup() function and then exposed as a key inside of the object we returned. We haven't covered what is returned from the object much so far, so for your info, here is what we have returned from our setup() function inside of ToDo.vue:

return {
  list,
  todo,
  showError,
  generateId,
  createNewToDoItem,
  onDeleteItem,
  displayError,
};

Here, list, todo, and showError are our stateful values, while everything else are functions we want to be able to call in other places of our app. Okay, coming back out from our tangent, when the page loads, we have todo set to an empty string, as such: const todo = ref(""). If this had some data already in there, such as const todo = ref("add some text here"): our input field would load with add some text here already inside the input field. Anyway, going back to having it as an empty string, whatever text we type inside the input field gets bound to todo.value. This is effectively two-way binding - the input field can update the ref() value and the ref() value can update the input field.

So looking back at the createNewToDoItem() code block from earlier, we see that we push the contents of todo.value into the list array - by pushing todo.value into list.value - and then update todo.value to an empty string.

We also used the same newId() function as used in the React example.

How do we delete from the list?

React:

const deleteItem = (id) => {
  setList(list.filter((item) => item.id !== id));
};

How did React do that?

So whilst the deleteItem() function is located inside ToDo.js, I was very easily able to make reference to it inside ToDoItem.js by firstly, passing the **deleteItem() **function as a prop on as such:

<ToDoItem key="{item.id}" item="{item}" deleteItem="{deleteItem}" />

This firstly passes the function down to make it accessible to the child. Then, inside the ToDoItem component, we do the following:

<button className="ToDoItem-Delete" onClick={() => deleteItem(item.id)}>
    -
</button>

All I had to do to reference a function that sat inside the parent component was to reference props.deleteItem. Now you may have noticed that in the code example, we just wrote deleteItem instead of props.deleteItem. This is because we used a technique known as destructuring which allows us to take parts of the props object and assign them to variables. So in our ToDoItem.js file, we have the following:

const ToDoItem = (props) => {
  const { item, deleteItem } = props;
};

This created two variables for us, one called item, which gets assigned the same value as props.item, and deleteItem, which gets assigned the value from props.deleteItem. We could have avoided this whole destructuring thing by simply using props.item and props.deleteItem, but I thought it was worth mentioning!

Vue:

function onDeleteItem(id) {
  list.value = list.value.filter((item) => item.id !== id);
}

How did Vue do that?

A slightly different approach is required in Vue. We essentially have to do three things here:

Firstly, on the element we want to call the function:

<button class="ToDoItem-Delete" @click="deleteItem(item.id)">-</button>

Then we have to create an emit function as a method inside the child component (in this case, ToDoItem.vue), which looks like this:

function deleteItem(id) {
  emit("delete", id);
}

Along with this, you’ll notice that we actually reference a function when we add ToDoItem.vue inside of ToDo.vue:

<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" />

This is what is known as a custom event-listener. It listens out for any occasion where an emit is triggered with the string of ‘delete’. If it hears this, it triggers a function called onDeleteItem. This function sits inside of ToDo.vue, rather than ToDoItem.vue. This function, as listed earlier, simply filters the id from the list.value array.

It’s also worth noting here that in the Vue example, I could have simply written the $emit part inside of the @click listener, as such:

<button class="ToDoItem-Delete" @click="emit("delete", item.id)">
    -
</button>

This would have reduced the number of steps down from 3 to 2, and this is simply down to personal preference.

In short, child components in React will have access to parent functions via props (providing you are passing props down, which is fairly standard practice and you’ll come across this loads of times in other React examples), whilst in Vue, you have to emit events from the child that will usually be collected inside the parent component.

How do we pass event listeners?

React:

Event listeners for simple things such as click events are straight forward. Here is an example of how we created a click event for a button that creates a new ToDo item:

<button className="ToDo-Add" onClick="{createNewToDoItem}">+</button>

Super easy here and pretty much looks like how we would handle an in-line onClick with vanilla JS. As mentioned in the Vue section, it took a little bit longer to set up an event listener to handle whenever the enter button was pressed. This essentially required an onKeyPress event to be handled by the input tag, as such:

<input
  type="text"
  placeholder="I need to..."
  value="{toDo}"
  onChange="{handleInput}"
  onKeyPress="{handleKeyPress}"
/>

This function essentially triggered the createNewToDoItem function whenever it recognised that the ‘enter’ key had been pressed, as such:

const handleKeyPress = (e) => {
  if (e.key === "Enter") {
    createNewToDoItem();
  }
};

Vue:

In Vue it is super straight-forward. We simply use the @ symbol, and then the type of event-listener we want to do. So for example, to add a click event listener, we could write the following:

<button class="ToDo-Add" @click="createNewToDoItem">+</button>

Note: @click is actually shorthand for writing v-on:click. The cool thing with Vue event listeners is that there are also a bunch of things that you can chain on to them, such as .once which prevents the event listener from being triggered more than once. There are also a bunch of shortcuts when it comes to writing specific event listeners for handling key strokes. I found that it took quite a bit longer to create an event listener in React to create new ToDo items whenever the enter button was pressed. In Vue, I was able to simply write:

<input type=”text” v-on:keyup.enter=”createNewToDoItem”/>

How do we pass data through to a child component?

React:

In react, we pass props onto the child component at the point where it is created. Such as:

<ToDoItem key="{item.id}" item="{item}" deleteItem="{deleteItem}" />

Here we see two props passed to the ToDoItem component. From this point on, we can now reference them in the child component via this.props. So to access the item.todo prop, we simply call props.item. You may have noticed that there's also a key prop (so technically we're actually passing three props). This is mainly for React's internals, as it makes things easier when it comes to making updates and tracking changes among multiple versions of the same component (which we have here because each todo is a copy of the ToDoItem component). It's also important to ensure your components have unique keys, otherwise React will warn you about it in the console.

Vue:

In Vue, we pass props onto the child component at the point where it is created. Such as:

<ToDoItem v-for="item in list" :item="item" @delete="onDeleteItem" :key="item.id" />

Once this is done, we then pass them into the props array in the child component, as such: props: [ "todo" ]. These can then be referenced in the child by their name — so in our case, todo. If you're unsure about where to place that prop key, here is what the entire export default object looks like in our child component:

export default {
  name: "ToDoItem",
  props: ["item"],
  setup(props, { emit }) {
    function deleteItem(id) {
      emit("delete", id);
    }
    return {
      deleteItem,
    };
  },
};

One thing you may have noticed is that when looping through data in Vue, we actually just looped through list rather than list.value. Trying to loop through list.value won't work here

How do we emit data back to a parent component?

React:

We firstly pass the function down to the child component by referencing it as a prop in the place where we call the child component. We then add the call to function on the child by whatever means, such as an onClick, by referencing props.whateverTheFunctionIsCalled — or whateverTheFunctionIsCalled if we have used destructuring. This will then trigger the function that sits in the parent component. We can see an example of this entire process in the section ‘How do we delete from the list’.

Vue:

In our child component, we simply write a function that emits a value back to the parent function. In our parent component, we write a function that listens for when that value is emitted, which can then trigger a function call. We can see an example of this entire process in the section ‘How do we delete from the list’.

And there we have it! 🎉

We’ve looked at how we add, remove and change data, pass data in the form of props from parent to child, and send data from the child to the parent in the form of event listeners. There are, of course, lots of other little differences and quirks between React and Vue, but hopefully the contents of this article has helped to serve as a bit of a foundation for understanding how both frameworks handle stuff.

If you’re interested in forking the styles used in this article and want to make your own equivalent piece, please feel free to do so! 👍

Github links to both apps:

Vue ToDo: https://github.com/sunil-sandhu/vue-todo-2020

React ToDo: https://github.com/sunil-sandhu/react-todo-2020

Translations

Chinese

The 2019 version of this article

https://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-2019-edition-42ba2cab9e56

The 2018 version of this article

https://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-in-react-and-vue-here-are-the-differences-e9a1ae8077fd