DEV Community

Cover image for Moving from React to Vue? Here's what to expect
Nikhil Verma
Nikhil Verma

Posted on • Updated on

Moving from React to Vue? Here's what to expect

I moved from a React to a Vue project two years ago from a company change. If you are about to undergo a similar journey, this post will help.

I will not cover the basic differences between them. I will instead attempt to explain the conceptual differences that I found when switching.

Embrace Vue's reactivity

My favourite part about Vue is that you can simply modify state. Vue will track the changes and update the computed properties and UI automatically. In React this is done explicitly via setters or using libraries which internally do the same.

Before joining the Vue project I had read how mutating state is bad because you can't track where the changes are coming from. While this is a valid point, in practice a reasonable data organisation and development discipline makes it a non issue.

The productivity gains from modifying a state property and knowing that the UI will update automatically are incredible.

This also integrates well with effects and computed properties. You don't need to pass dependencies in your effects/computed properties, it "just works".

Here is an example code that works in Vue out of the box.

/* state.js */
import { ref } from "vue";
export const count = ref(0);

/* App.vue */
<script setup lang="ts">
import { count } from "./state";
</script>

<template>
    <div>The count is {{ count }}</div>
    <button @click="count++">count++</button>
</template>
Enter fullscreen mode Exit fullscreen mode

Doing something similar in React will need a state management library.

Know your Vue version

There is a big difference between Vue 2 and 3 experiences. With the latter being more polished and better supported in the modern web development experience.

I have written about the pain we went through when migrating from Vue 2 to 3.

In React, you wouldn't have to worry about this as there haven't been major breaking API changes between version updates.

Vue 2 will reach end of life support on December 31st, 2023. My advice would be to plan a migration to Vue 3 as soon as possible.

Understand the core difference between Vue templates and JSX

API differences aside, you must have a basic understanding of how Vue templates operate internally and how they differ from JSX.

Vue templates are closer to HTML, JSX is closer to Javascript. (You might have heard the phrase "JSX is Javascript").

In JSX you can refer to local variables, use Javascript expressions and use JSX inline with other JS code.

In Vue you must define them inside a template block. They can only refer to values you have either provided in your component definition (options API) or present in your setup scope (composition API).

You also can't easily split Vue templates into inline code like JSX. They must be defined in a separate SFC file or a string template inside a render function (which needs runtime compilation).

An example of what I am talking about. Let's take two bits of Vue and React code. They basically behave in the same way. And let's compare their code (as you would write it) and their compiled output.

React

import HelloWorld from "./components/HelloWorld";
import Header from "./components/Header";
import HeaderAlt from "./components/HeaderAlt";

const App = () => {
  const isAlternateHeader = Math.random() > 0.5;
  const items = [1, 2, 3];

  return (
    <HelloWorld
      msg="Vite + React"
      header={isAlternateHeader ? <HeaderAlt /> : <Header />}
    >
      {items.map((item) => (
        <div {...{ [isAlternateHeader ? "alt-header" : "header"]: true }}>
          {item}
        </div>
      ))}

      {isAlternateHeader ? "Alt header" : "Header"}
    </HelloWorld>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

React (as compiled by Babel)

import HelloWorld from "./components/HelloWorld";
import Header from "./components/Header";
import HeaderAlt from "./components/HeaderAlt";
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";

const App = () => {
  const isAlternateHeader = Math.random() > 0.5;
  const items = [1, 2, 3];
  return /*#__PURE__*/_jsxs(HelloWorld, {
    msg: "Vite + React",
    header: isAlternateHeader ? /*#__PURE__*/_jsx(HeaderAlt, {}) : /*#__PURE__*/_jsx(Header, {}),
    children: [items.map(item => /*#__PURE__*/_jsx("div", {
      [isAlternateHeader ? "alt-header" : "header"]: true,
      children: item
    })), isAlternateHeader ? "Alt header" : "Header"]
  });
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Now let's look at a similar Vue code

Vue

<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import Header from "./components/Header.vue";
import HeaderAlt from "./components/HeaderAlt.vue";

const isAlternateHeader = Math.random() > 0.5;
const items = [1, 2, 3];
const dynamicAttribute = isAlternateHeader ? 'alt-header' : 'header';
</script>

<template>
  <HelloWorld msg="Vite + Vue">
    <template #header>
      <HeaderAlt v-if="isAlternateHeader" />
      <Header v-else />
    </template>

    <div :[dynamicAttribute]="true" v-for="item in items" :key="item">
      {{ item }}
    </div>

    {{ isAlternateHeader ? 'Alt header' : 'Header' }}
  </HelloWorld>
</template>
Enter fullscreen mode Exit fullscreen mode

Vue (compiled by Vue SFC Playground)

import { defineComponent as _defineComponent } from "vue";
import {
  openBlock as _openBlock,
  createBlock as _createBlock,
  renderList as _renderList,
  Fragment as _Fragment,
  createElementBlock as _createElementBlock,
  unref as _unref,
  toDisplayString as _toDisplayString,
  normalizeProps as _normalizeProps,
  createElementVNode as _createElementVNode,
  createTextVNode as _createTextVNode,
  withCtx as _withCtx,
} from "vue";

import HelloWorld from "./components/HelloWorld.vue";
import Header from "./components/Header.vue";
import HeaderAlt from "./components/HeaderAlt.vue";

const __sfc__ = _defineComponent({
  __name: "App",
  setup(__props) {
    const isAlternateHeader = Math.random() > 0.5;
    const items = [1, 2, 3];
    const dynamicAttribute = isAlternateHeader ? "alt-header" : "header";

    return (_ctx, _cache) => {
      return (
        _openBlock(),
        _createBlock(
          HelloWorld,
          { msg: "Vite + Vue" },
          {
            header: _withCtx(() => [
              isAlternateHeader
                ? (_openBlock(), _createBlock(HeaderAlt, { key: 0 }))
                : (_openBlock(), _createBlock(Header, { key: 1 })),
            ]),
            default: _withCtx(() => [
              (_openBlock(),
              _createElementBlock(
                _Fragment,
                null,
                _renderList(items, (item) => {
                  return _createElementVNode(
                    "div",
                    _normalizeProps({
                      [_unref(dynamicAttribute) || ""]: true,
                      key: item,
                    }),
                    _toDisplayString(item),
                    17 /* TEXT, FULL_PROPS */
                  );
                }),
                64 /* STABLE_FRAGMENT */
              )),
              _createTextVNode(
                " " +
                  _toDisplayString(isAlternateHeader ? "Alt header" : "Header"),
                1 /* TEXT */
              ),
            ]),
            _: 1 /* STABLE */,
          }
        )
      );
    };
  },
});
__sfc__.__file = "src/App.vue";
export default __sfc__;
Enter fullscreen mode Exit fullscreen mode

The translation from JSX to JS is simple because tags map to function calls and props map to object properties.

Vue has a more custom DSL where more code is needed to translate it to an executable Javascript code. This also comes into play when using it with Typescript.

Vue's Typescript support is... fine

If you are on a < Vue 2.7 project, you will have a bad experience with Typescript. You might even have to use other libraries to rewrite your Vue components using classes.

Even with a modern Vue 3 project, the Typescript support can be described as "fine". Since Vue templates don’t easily translate to Javascript, it has to do complex compilation to get the types working. Even then, certain things like context (provide/inject) and event arguments need typing manually.

Do you remember the earlier app example? Here is the internal Typescript representation of it's code using Volar (gist.github.com). I had to use a gist because the generated code is huge to allow proper type inference to work.

This is the outcome of using a custom DSL in Vue, it has it's tradeoffs.

Scoped styles out of the box

Vue also provides scoped styles out of the box. This means the following code will not leak CSS.

<style scoped>
.example {
  color: red;
}
</style>

<template>
  <div class="example">hi</div>
</template>
Enter fullscreen mode Exit fullscreen mode

Final thoughts

I still prefer JSX over Vue templates but I vastly prefer the ref/reactive/computed systems of Vue.

My ideal UI library will offer first class template syntax of JSX and reactivity of Vue. Vue does support JSX but it has it's own set of gotchas.

Vue was designed with a certain set of tradeoffs, and it does them really well. If you have a Vue 3 codebase you will not find it too difficult to switch from React.

If you have a Vue 2 codebase however... Read my post here.

Top comments (1)

Collapse
 
aarone4 profile image
Aaron Reese

Of you like HTML in your JS then use React. If you prefer JS in your HTML then use Vue.😁
For me the template syntax is just 'cleaner' especially if you have nested array iterators. I also struggled with the Composition API and the fact that you have to explicitly export all your variables and that they are objects and you have to use ref.value outside of the template. The Options API was easier with the collection of reactive data, computed and watchers into their own objects.