Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.
If you have looked at react a long time ago and got scared away by some of its verbosity (I mean you ComponentDidMount
, ComponentWillReceiveProps
, getDerivedStateFromProps
etc.), have a look again. Hooks take functional components to the next level. And it comes with all the benefits you could imagine, no classes, no this
, no boilerplate. Turns out I am not alone on this, as some of these points are also mentioned in the official docs talking about the motivation behind hooks.
Let's compare some common vue things and implement them using react hooks, then list up the pros and cons of each tool. This is not to convince you to drop vue over react, especially seeing that vue is moving in the same direction (more on that at the end). But it is always good to get a sense of how the other frameworks achieve common tasks, as something similar might also become the future of vue.
The component itself
The minimum we need for a vue single file component would be the following setup
// Counter.vue
<template>
<div>0</div>
</template>
<script>
export default {}
</script>
And here is the same thing in react
function Counter() {
return <div>0</div>
}
Note that the react component doesn't necessarily have to live in its own file, since it's just a function.
Working with state
Vue
// Counter.vue
<template>
<button @click="increment">{{ count }}</button>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
and react
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
return <button onClick={increment}>{ count }</button>
}
As you can see, react's useState
returns a tuple with a set function as the second argument. In vue, you can directly set the value to update the state.
With hooks, Whenever our state/props get updated, the Counter
method is executed again. Only the first time though it initiates the count
variable with 1. That's basically the whole deal about hooks. This concept is one of the few that you have to understand with hooks.
vue pros/cons
(+) predefined structure
(-) you can not just import something and use it in the template. It has to be laid out in one of the various concepts of vue data
, methods
, computed
, $store
etc. This also makes some values needlessly reactive and might cause confusion (why is this reactive? Does it change? Where?)
react pros/cons
(+) It's just a function
(-) Actually it's a function that gets executed every time state or props change. That way of thinking is likely no problem for those used to the old stateless functional components of react, but for people who exclusively used vue, a new way of thinking is required. It just doesn't come off natural at first.
(-) Hooks have various rules on where and how you have to use them.
Passing props
// Counter.vue
<template>
<div>
<h1>{{ title }}</h1>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
props: {
title: String
},
methods: {
increment() {
this.count++
}
}
}
</script>
and react
import { useState } from 'react'
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
return (
<>
<h2>{title}</h2>
<button onClick={increment}>{count}</button>
</>
)
}
vue pros/cons
(+) You can be specific about the types of your props (without TS)
(-) access the same way as state (this.xxx), but actually behaves differently (e.g. assigning a new value throws a warning). This makes beginners think they can just go ahead and update props.
react pros/cons
(+) easy to understand -> props are just function arguments
Child components
Let's extract the button into a child component.
vue
// Button.vue
<template>
<button @click="$emit('handle-click')">
{{ value }}
</button>
</template>
<script>
export default {
props: ['value']
}
</script>
// Counter.vue
<template>
<div>
<h1>{{ title }}</h1>
<Button @handle-click="increment" :value="count" />
</div>
</template>
<script>
import Button from './Button'
export default {
components: {
Button,
},
data() {
return {
count: 1
}
},
props: ['title'],
methods: {
increment() {
this.count++
}
}
}
</script>
vue introduces a "new" concept events
at this point.
The react counterpart
import { useState } from 'react'
function Button({value, handleClick}) {
return <button onClick={handleClick}>{value}</button>
}
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
return (
<>
<h2>{title}</h2>
<Button value={count} handleClick={increment}/>
</>
)
}
vue pros/cons
(+) clear separation of concerns
(+) events play very nice with vue devtools
(+) Events come with modifiers that make the code super clean. E.g. @submit.prevent="submit"
< applies event.preventDefault()
(-) weird casing rules
(-) sort of an additional concept to learn (events). Actually events are similar to native events in the browser. One of the few differences would be that they don't bubble up.
react pros/cons
(+) we are not forced to create separate files
(+) no concepts of events -> just pass the function in as a prop. To update props, you can also just pass in a function as a prop
(+) overall shorter (at least in this derived example)
Some of the pros/cons are contradicting, this is because in the end it all comes down to personal preference. One might like the freedom of react, while others prefer the clear structure of vue.
Slots
Vue introduces yet another concept when you want to pass template to a child component. Let's make it possible to pass more than a string to the button.
// Button.vue
<template>
<div>
<button @click="$emit('handle-click')">
<slot>Default</slot>
</button>
<slot name="afterButton"/>
</div>
</template>
<script>
export default {}
</script>
// Counter.vue
<template>
<div>
<h1>{{ title }}</h1>
<Button @handle-click="increment">
<strong>{{ count }}</strong>
<template v-slot:afterButton>
Some content after the button...
</template>
</Button>
</div>
</template>
<script>
import Button from './Button'
export default {
components: {
Button,
},
data() {
return {
count: 1
}
},
props: ['title'],
methods: {
increment() {
this.count++
}
}
}
</script>
<strong>{{ count }}</strong>
will go inside <slot></slot>
since it is the default/unnamed slot. Some content after the button...
will be placed inside <slot name="afterButton"/>
.
And in react
import { useState } from 'react'
function Button({AfterButton, handleClick, children}) {
return (
<>
<button onClick={handleClick}>
{children}
</button>
<AfterButton />
</>
)
}
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
return (
<>
<h2>{title}</h2>
<Button value={count} handleClick={increment} AfterButton={() => 'some content...'}>
<strong>{ count }</strong>
</Button>
</>
)
}
vue pros/cons
(-) slots can be confusing. Especially when you send data from the child component to the slot.
(-) Passing slots down multiple components is even more confusing
(-) another concept to learn
These are consequences of vue using a custom templating language. It mostly works, but with slots it can become complicated.
Slots will get simplified in vue 3
react pros/cons
(+) no new concept - Since components are just functions, just create such a function and pass it in as a prop
(+) Doesn't even have to be a function. You can save template(jsx) in a variable and pass it around. This is exactly what happens with the special children
prop.
Computed fields
Let's simplify the examples again
// Counter.vue
<template>
<div>
<h1>{{ capitalizedTitle }}</h1>
<button @click="increment">{{ count }}</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
props: ['title'],
computed: {
capitalizedTitle() {
return title.toUpperCase()
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
react
import { useState, useMemo } from 'react'
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
const capitalizedTitle = title.toUpperCase()
return (
<>
<h2>{capitalizedTitle}</h2>
<button onClick={increment}>{count}</button>
</>
)
}
In vue, computed fields serve not one, but two purposes. They keep the template clean and at the same time provide caching.
In react, we can simply declare a variable that holds the desired value to solve the problem of keeping the template clean. (const capitalizedTitle = title.toUpperCase()
)
In order to cache it as well, we can make use of react's useMemo
hook.
const capitalizedTitle = useMemo(() => title.toUpperCase(), [title])
In the second argument we have to specify the fields required to invalidate the cache if any of the fields' value changes.
useMemo works like this:
title changes outside of component ->
Counter
function runs since prop got updated ->useMemo
realizes that the title changed, runs the function passed in as the first argument, caches the result of it and returns it.
vue pros/cons
(+) nice and clear separation of concerns
(-) you define computed fields in functions, but access them like state/props. This makes perfect sense if you think about it, but I have received questions about this repeatedly by peers.
(-) There is some magic going on here. How does vue know when to invalidate the cache?
(-) Computed fields serve two purposes
react pros/cons
(+) To keep the template clean, there is no new concept to learn, just save it in a variable, and use that variable in the template
(+) You have control over what gets cached and how
Watch
// Counter.vue
<template>
<button @click="increment">{{ capitalizedTitle }}</button>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
watch: {
count() {
console.log(this.count)
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
react
import { useState, useEffect } from 'react'
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
useEffect(() => {
console.log(count)
}, [count])
return (
<button onClick={increment}>{count}</button>
)
}
useEffect
works pretty much the same way as useMemo
, just without the caching part.
setCount
->Counter
function runs ->useEffect
realizes that the count changed and will run the effect.
vue pros/cons
(+) clean, easily understandable, nailed it!
react pros/cons
(+) You can specify multiple fields instead of just one field
(-) The purpose of useEffect
is not as clear as vue's watch
. This is also because useEffect
is used for more than one thing. It deals with any kind of side effects.
mounted
Doing something when a component has mounted is a good place for ajax requests.
vue
// Counter.vue
<template>
<button @click="increment">{{ capitalizedTitle }}</button>
</template>
<script>
export default {
data() {
return {
count: 1
}
},
mounted() {
// this.$http.get...
},
methods: {
increment() {
this.count++
}
}
}
</script>
and react
import { useState, useEffect } from 'react'
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
useEffect(() => {
// ajax request...
}, [])
return (
<button onClick={increment}>{count}</button>
)
}
You can use the same useEffect
as before, but this time specify an empty array as the second argument. It will execute once, and since there is no state specified like before ([count]
), it will never evaluate a second time.
vue pros/cons
(+) clean and easy.
(-) Initiating something and cleaning up after it has to be in two different methods, which makes you jump unnecessarily and forces you to save variables somewhere else entirely (more on that in a moment)
react pros/cons
(-) Very abstract. I would have preferred a dedicated method for it instead. Cool thing is, I have the freedom to just make it.
(-) useEffect
callback is not allowed to return promises (causes race conditions)
(+) clean up in very same function:
Turns out useEffect
comes with one rather interesting and neat feature. If you return a function within useEffect
, it is used when the component gets unmounted/destroyed. This sounds confusing at first, but saves you some temporary variables.
Look at this
import { useState, useEffect } from 'react'
function App() {
const [showsCount, setShowsCount] = useState(true);
return (
<div className="App">
<button onClick={() => setShowsCount(!showsCount)}>toggle</button>
{showsCount && <Counter />}
</div>
);
}
function Counter({ title }) {
const [count, setCount] = useState(1)
const increment = () => setCount(count+1)
useEffect(() => {
const interval = setInterval(() => {
increment()
console.log("interval")
}, 1000)
return function cleanup() {
clearInterval(interval)
}
}, [])
return (
<button>{count}</button>
)
}
The interesting part is inside useEffect
. In the same scope we are able to create and clear an interval. With vue, we would have to initiate the variable first somewhere else, so that we can fill it in mounted
and cleanup inside destroy
.
Others
vue
(+) v-model
directive
(+) first party tools like SSR, VueX and vue-router that play very nice with devtools
(+) Scoped CSS out of the box. Super easy to use SCSS
(+) Feels more like traditional web development and makes onboarding easier
react
(+) More and more things become first party and part of the react core library (hooks, code splitting, etc.)
(+) many libraries to choose from
Conclusion
vue limits you in certain ways, but by that, it also structures your code in a clean and consistent way.
React doesn't limit you much, but in return, you have a lot more responsibility to maintain clean code. This I think became much easier with the introduction of hooks.
But then of course, with all the competition going on, vue is not going to ignore the benefits of react hooks and has already released an rfc for function-based components. It looks promising and I am excited where it will lead to!
Top comments (1)
Thanks Michael for this article. I'm using Vue and know very little about React and it's interesting to have this perspective.
What you describe is the recommended and most common approach for making a Vue project but it's not the only one !
Some examples :
Vue components can be declared with render functions and do not have to be in their own files :
vuejs.org/v2/guide/render-function...
You can also pass a function as a prop instead of relying on events.
Computed properties are what you want most of the time. But if you need to have more control over caching you are also free to use methods or a combination of methods and state.
Computed properties are not magic, vue registers the other reactive variables your property depends on and will "invalidate the cache" when one of them has changed.
Cheers