DEV Community

loading...
Cover image for Write Vue like you write React

Write Vue like you write React

Gábor Soós
Helping others create quality software w/ Javascript · Blogger · DogDad · Powerlifter · Gearhead
・3 min read

With the Vue 3 Composition API, you can write functional components. It's possible also with React, but React has different templating: JSX. What happens if we write Vue functional components with JSX templates? Are they similar to React functional components?

Let's look at how both frameworks functional components work and how similar/different they're through. The component we'll use for this purpose is a counter, which counts clicks on a button. Additionally it receives a limit parameter: when this limit is reached the component notifies its parent.

We'll create the React component first and then look at the Vue equivalent.

React

import { useState, useEffect } from 'react';

export const Counter = ({ limit, onLimit }) => {
  const [count, setCount] = useState(0);
  const handler = () => setCount(count + 1);

  useEffect(
    () => (count >= limit) ? onLimit() : null,
    [count]
  );

  return <button type="button" onClick={handler}>
    Count: {count}
  </button>;
};
Enter fullscreen mode Exit fullscreen mode

React requires a plain Javascript function that returns a JSX template to create a component. This function reruns whenever the component's state changes. You can create such state with the useState method. State variables are plain Javascript constructs that persist value between reruns. Every other variable is lost between state changes. You can test it with a console.log statement at the top of the function.

The component has a limit and a method which can be used to notify the parent component. We want to check the current value whenever it is incremented. The useEffect function serves as a checker and runs the callback whenever the dependencies in the second argument change.

In a nutshell: React component is a plain function with plain Javascript state values that reruns on every state change and returns JSX.

Vue

import { defineComponent, ref, watchEffect } from 'vue';

export const Counter = defineComponent({
  props: ['limit', 'onLimit'],
  setup(props) {
    const count = ref(0);
    const handler = () => count.value++;

    watchEffect(
      () => (count.value >= props.limit) ? props.onLimit() : null
    );

    return () => <button type="button" onClick={handler}>
      Count: {count.value}
    </button>;
  }
});
Enter fullscreen mode Exit fullscreen mode

The plain function equivalent in Vue is the setup method within the component object. The setup method also receives props as an input parameter, but instead of JSX, it returns a function that returns JSX. You may wonder why.

The reason is because the setup function only runs once and only the returned function runs on state change. If the setup function only runs once, how can Vue detect changes? The trick lies in Vue's reactivity system. The ref function wraps the original value inside a Javascript Proxy object. Every modification runs through this proxy which notifies Vue to rerender that component. If we modify the original value directly that change will be ignored by the framework.

The limit and notifier function come as a function parameter, but in Vue we haven't used destructuring. The reason is that props is also a Proxy object and if we destructure it, we lose its reactivity (if it changes, nothing would happen). To check value changes, we have to use the useEffect function. In contrary to React, we don't have to define the watched dependencies, Vue does it automatically as it knows about which state variables (Proxies) we use inside the callback.

For Vue developers using a function instead of an event to notify the parent might be unusual. Some say it's an anti-pattern in Vue, but to make it as close to React as possible I've chosen this way.

Summary

Both framework can create a component with a single function. The Vue functional component is a function with state values wrapped inside Proxies that only runs once and only the returned function reruns and returns JSX. The React functional component is a function with state values as plain Javascript constructs that reruns on every state change and returns JSX directly.

The difference lies in the way how each framework solves the problem of reactivity: React is the stateless reference comparing solution, Vue is the stateful Proxy based solution.

It was an interesting and fun experiment to try to write the same component in different frameworks with similar approach as identically as possible. I hope you find it also interesting. You can also give it a try in my Vue 3 Vite playground.

Discussion (8)

Collapse
shinigami92 profile image
Shinigami • Edited

I tried this last week myself

Is there a way to get type/variable-name checking within the (j,t)sx part?

import { defineComponent } from '@vue/composition-api';
import type { VNode } from 'vue';

export default defineComponent({
  name: 'ComponentName',
  props: { label: String },
  render(): VNode {
    return <div>{ this.lable }</div>;
  }
});
Enter fullscreen mode Exit fullscreen mode

So in this example I misspelled label in the render function, but TSX doesn't provide me an error for that :(


Collapse
shinigami92 profile image
Shinigami

Seems I need at least something like this

interface Data {
  label: string;
}
export default defineComponent<unknown, Data, Data>( //...
Enter fullscreen mode Exit fullscreen mode

Then this.lable is an error

Collapse
emretoprak profile image
Emre Toprak

I Love Vue because i hate JSX. :)

Collapse
jfbrennan profile image
Jordan Brennan

The code is clearly worse, so what's the benefit?

Collapse
vuesomedev profile image
Gábor Soós Author

JSX or the functional api?

Can you. elaborate on what is worse? I feel JSX as a different approach, not a better/worse one - different people different tastes.

Collapse
jfbrennan profile image
Jordan Brennan • Edited

One reason devs like Vue is because it's not like React. So what is the benefit of writing this React-like component:

import { defineComponent, ref, watchEffect } from 'vue';

export const Counter = defineComponent({
  props: ['limit', 'onLimit'],
  setup(props) {
    const count = ref(0);
    const handler = () => count.value++;

    watchEffect(
      () => (count.value >= props.limit) ? props.onLimit() : null
    );

    return () => <button type="button" onClick={handler}>
      Count: {count.value}
    </button>;
  }
});
Enter fullscreen mode Exit fullscreen mode

compared to regular Vue:

<template>
<button type="button" v-on:click="increment">Count: {{ count }}</button>
</template>

<script>
export default {
  props: ['limit', 'onLimit'],
  data() {
    return { count: 0 }
  },

  watch: {
    count: function(count) { count >= this.limit && this.onLimit() }
  },

  methods: {
    increment() { this.count++ }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
vuesomedev profile image
Gábor Soós Author

You can extract the business logic as a plain function. The JSX templating is just for the experiment. JSX in many cases suffers performance penalty compared to Vue templates.

Collapse
leefreemanxyz profile image
Lee Freeman

This is beautiful, such a huge improvement – no longer will I suffer from magic stringly-typed variables appearing in templates <3.