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:
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
<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