[go: up one dir, main page]

DEV Community

Michael Z
Michael Z

Posted on • Edited on • Originally published at michaelzanggl.com

React Hooks for Vue developers

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>
Enter fullscreen mode Exit fullscreen mode

And here is the same thing in react

function Counter() {
    return <div>0</div>
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

and react

import { useState } from 'react'

function Counter() {
    const [count, setCount] = useState(1)
    const increment = () => setCount(count+1)

    return <button onClick={increment}>{ count }</button>
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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>
Enter fullscreen mode Exit fullscreen mode

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}/>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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>
Enter fullscreen mode Exit fullscreen mode

<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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
gmeral profile image
gmeral

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