React for Vue developers

| 14 min read

For the past three years, I’ve been using both React and Vue in different projects, ranging from smaller websites to large scale apps.

Last month I wrote a post about why I prefer React over Vue. Shortly after I joined Adam Wathan on Full Stack Radio to talk about React from a Vue developer’s perspective.

We covered a lot of ground on the podcast, but most things we talked about could benefit from some code snippets to illustrate their similaraties and differences.

This post is a succinct rundown of most Vue features, and how I would write them with React in 2019 with hooks.

Did I miss anything? Are there other comparisons you’d like to see? Or do you just want to share your thoughts about Vue or React? Talk to me on Twitter!

Table of contents

Templates

React alternative: JSX

Vue uses HTML strings with some custom directives for templating. They recommend using .vue files to seperate the templates and script (and optionally styles).

<!-- Greeter.vue -->

<template>
  <p>Hello, {{ name }}!</p>
</template>

<script>
export default {
  props: ['name']
};
</script>

React uses JSX, which is an extension of ECMAScript.

export default function Greeter({ name }) {
  return <p>Hello, {name}!</p>;
}

Conditional rendering

React alternative: Logical && operator, ternary statements, or early returns

Vue uses v-if, v-else and v-else-if directives to conditionally render parts of a template.

<!-- Awesome.vue -->

<template>
  <article>
    <h1 v-if="awesome">Vue is awesome!</h1>
  </article>
</template>

<script>
export default {
  props: ['awesome']
};
</script>

React doesn’t support directives, so you need to use the language to conditionally return parts of a template.

The && operator provides a succinct way to write an if statement.

export default function Awesome({ awesome }) {
  return (
    <article>
      {awesome && <h1>React is awesome!</h1>};
    </article>
  );
}

If you need an else clause, use a ternary statement instead.

export default function Awesome({ awesome }) {
  return (
    <article>
      {awesome ? (
        <h1>React is awesome!</h1>
      ) : (
        <h1>Oh no 😢</h1>
      )};
    </article>
}

You could also opt to keep the two branches completely separated, and use an early return instead.

export default function Awesome({ awesome }) {
  if (!awesome) {
    return (
      <article>
        <h1>Oh no 😢</h1>
      </article>
    );
  }

  return (
    <article>
      <h1>React is awesome!</h1>
    </article>
  );
}

List rendering

React alternative: Array.map

Vue uses the v-for directive to loop over arrays and objects.

<!-- Recipe.vue -->

<template>
  <ul>
    <li v-for="(ingredient, index) in ingredients" :key="index">
      {{ ingredient }}
    </li>
  </ul>
</template>

<script>
export default {
  props: ['ingredients']
};
</script>

With React, you can “map” the array to a set of elements using the built in Array.map function.

export default function Recipe({ ingredients }) {
  return (
    <ul>
      {ingredients.map((ingredient, index) => (
        <li key={index}>{ingredient}</li>
      ))}
    </ul>
  );
}

Iterating objects is a bit trickier. Vue allows you to use the same v-for directive for keys & values.

<!-- KeyValueList.vue -->

<template>
  <ul>
    <li v-for="(value, key) in object" :key="key">
      {{ key }}: {{ value }}
    </li>
  </ul>
</template>

<script>
export default {
  props: ['object'] // E.g. { a: 'Foo', b: 'Bar' }
};
</script>

I like to use the built in Object.entries function with React to iterate over objects.

export default function KeyValueList({ object }) {
  return (
    <ul>
      {Object.entries(object).map(([key, value]) => (
        <li key={key}>{value}</li>
      ))}
    </ul>
  );
}

Class and style bindings

React alternative: Manually pass props

Vue automatically binds class and style props to the outer HTML element of a component.

<!-- Post.vue -->

<template>
  <article>
    <h1>{{ title }}</h1>
  </article>
</template>

<script>
export default {
  props: ['title'],
};
</script>

<!--
<post
  :title="About CSS"
  class="margin-bottom"
  style="color: red"
/>
-->

With React, you need to manually pass className and style props. Note that style must be an object with React, strings are not supported.

export default function Post({ title, className, style }) {
  return (
    <article className={className} style={style}>
      {title}
    </article>
  );
}

{/* <Post
  title="About CSS"
  className="margin-bottom"
  style={{ color: 'red' }}
/> */}

If you want to pass down all remaining props, the object rest spread operator comes in handy.

export default function Post({ title, ...props }) {
  return (
    <article {...props}>
      {title}
    </article>
  );
}

If you miss Vue’s excellent class API, look into Jed Watson’s classnames library.

Props

React alternative: Props

Props behave pretty much the same way in React as Vue. One minor difference: React components won’t inherit unknown attributes.

<!-- Post.vue -->

<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: ['title'],
};
</script>
export default function Post({ title }) {
  return <h3>{title}</h3>;
}

Using expressions as props in Vue is possible with a : prefix, which is an alias for the v-bind directive. React uses curly braces for dynamic values.

<!-- Post.vue -->

<template>
  <post-title :title="title" />
</template>

<script>
export default {
  props: ['title'],
};
</script>
export default function Post({ title }) {
  return <PostTitle title={title} />;
}

Data

React alternative: The useState hook

In Vue the data option is used to store local component state.

<!-- ButtonCounter.vue -->

<template>
  <button @click="count++">
    You clicked me {{ count }} times.
  </button>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
};
</script>

React exposes a useState hook which returns a two-element array containing the current state value and a setter function.

import { useState } from 'react';

export default function ButtonCounter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

You can choose whether you prefer to distribute state between multiple useState calls, or keep it in a single object.

import { useState } from 'react';

export default function ProfileForm() {
  const [name, setName] = useState('Sebastian');
  const [email, setEmail] = useState('sebastian@spatie.be');

  // ...
}
import { useState } from 'react';

export default function ProfileForm() {
  const [values, setValues] = useState({
    name: 'Sebastian',
    email: 'sebastian@spatie.be'
  });

  // ...
}

v-model

v-model is a convenient Vue directive that combines passing down a value prop with listening to an input event. This makes it look like Vue does two-way binding, while it’s still just “props down, events up” under the hood.

<!-- Profile.vue -->

<template>
  <input type="text" v-model="name" />
</template>

<script>
export default {
  data() {
    return {
      name: 'Sebastian'
    }
  }
};
</script>

Vue expands the v-model directive to the following:

<template>
  <input
    type="text"
    :value="name"
    @input="name = $event.target.value"
  />
</template>

React doesn’t have a v-model equivalent. You always need to be explicit:

import { useState } from 'react';

export default function Profile() {
  const [name, setName] = useState('Sebastian');

  return (
    <input
      type="text"
      value={name}
      onChange={event => setName(event.target.name)}
    />
  );
}

Computed properties

React alternative: Variables, optionally wrapped in useMemo

Vue has computed properties for two reasons: to avoid mixing logic and markup in templates, and to cache complex computations in a component instance.

Without computed properties:

<!-- ReversedMessage.vue -->

<template>
  <p>{{ message.split('').reverse().join('') }}</p>
</template>

<script>
export default {
  props: ['message']
};
</script>
export default function ReversedMessage({ message }) {
  return <p>{message.split('').reverse().join('')}</p>;
}

With React, you can extract the computation from the template by assigning the result to a variable.

<!-- ReversedMessage.vue -->

<template>
  <p>{{ reversedMessage }}</p>
</template>

<script>
export default {
  props: ['message'],

  computed: {
    reversedMessage() {
      return this.message.split('').reverse().join('');
    }
  }
};
</script>
export default function ReversedMessage({ message }) {
  const reversedMessage = message.split('').reverse().join('');

  return <p>{reversedMessage}</p>;
}

If performance is a concern, the computation can be wrapped in a useMemo hook. useMemo requires a callback that returns a computed result, and an array of dependencies.

In the following example, reversedMessage will only be recomputed if the message dependency changes.

import { useMemo } from 'react';

export default function ReversedMessage({ message }) {
  const reversedMessage = useMemo(() => {
    return message.split('').reverse().join('');
  }, [message]);

  return <p>{reversedMessage}</p>;
}

Methods

React alternative: Functions

Vue has a methods option to declare functions that can be used throughout the component.

<!-- ImportantButton.vue -->

<template>
  <button onClick="doSomething">
    Do something!
  </button>
</template>

<script>
export default {
  methods: {
    doSomething() {
      // ...
    }
  }
};
</script>

In React you can declare plain functions inside our component.

export default function ImportantButton() {
  function doSomething() {
    // ...
  }

  return (
    <button onClick={doSomething}>
      Do something!
    </button>
  );
}

Events

React alternative: Callback props

Events are essentially callbacks that are called when something happened in the child component. Vue sees events as a first-class citizen, so you can “listen” to them with @, which is shorthand for the v-on directive.

<!-- PostForm.vue -->

<template>
  <form>
    <button type="button" @click="$emit('save')">
      Save
    </button>
    <button type="button" @click="$emit('publish')">
      Publish
    </button>
  </form>
</template>

Events don’t have any special meaning in React, they’re just callback props will be called by the child component.

export default function PostForm({ onSave, onPublish }) {
  return (
    <form>
      <button type="button" onClick={onSave}>
        Save
      </button>
      <button type="button" onClick={onPublish}>
        Publish
      </button>
    </form>
  );
}

Event modifiers

React alternative: Higher order functions if you really want

Vue has a few modifiers like prevent and stop to change the way an event is handled without touching it’s handler.

<!-- AjaxForm.vue -->

<template>
  <form @submit.prevent="submitWithAjax">
    <!-- ... -->
  </form>
</template>

<script>
export default {
  methods: {
    submitWithAjax() {
      // ...
    }
  }
};
</script>

There’s no modifier syntax in React. Preventing defaults and stopping propagation is mostly handled in the callback.

export default function AjaxForm() {
  function submitWithAjax(event) {
    event.preventDefault();
    // ...
  }

  return (
    <form onSubmit={submitWithAjax}>
      {/* ... */}
    </form>
  );
}

If you really want to have something modifier-like, you could use a higher order function.

function prevent(callback) {
  return (event) => {
      event.preventDefault();
      callback(event);
  };
}

export default function AjaxForm() {
  function submitWithAjax(event) {
    // ...
  }

  return (
    <form onSubmit={prevent(submitWithAjax)}>
      {/* ... */}
    </form>
  );
}

Lifecycle methods

React alternative: The useEffect hook

A common case for lifecycle methods is to set up and tear down third party libraries.

<template>
  <input type="text" ref="input" />
</template>

<script>
import DateTimePicker from 'awesome-date-time-picker';

export default {
  mounted() {
   this.dateTimePickerInstance =
     new DateTimePicker(this.$refs.input);
  },

  beforeDestroy() {
    this.dateTimePickerInstance.destroy();
  }
};
</script>

With useEffect, you can declare a “side effect” that needs to run after a render. When you return a callback from useEffect, it will be invoked when the effect gets cleaned up. In this case, when the component is destroyed.

import { useEffect, useRef } from 'react';
import DateTimePicker from 'awesome-date-time-picker';

export default function Component() {
  const dateTimePickerRef = useRef();

  useEffect(() => {
    const dateTimePickerInstance =
      new DateTimePicker(dateTimePickerRef.current);

    return () => {
      dateTimePickerInstance.destroy();
    };
  }, []);

  return <input type="text" ref={dateTimePickerRef} />;
}

This looks similar to registering a beforeDestroy listener in mounted in a Vue component.

<script>
export default {
  mounted() {
    const dateTimePicker =
      new DateTimePicker(this.$refs.input);

    this.$once('hook:beforeDestroy', () => {
      dateTimePicker.destroy();
    });
  }
};
</script>

Similar to useMemo, useEffect accepts an array of dependencies as a second parameter.

Without any specified dependencies, the effect will run after every render, and will clean up before every next render. This functionality is similar to a combination of mounted, updated, beforeUpdate and beforeDestroy.

useEffect(() => {
    // Happens after every render

    return () => {
        // Optional; clean up before next render
    };
});

If you specify that the effect has no dependencies, the effect will only run when the component renders the first time, because it has no reason to update. This functionality is similar to a combination of mounted, and beforeDestroyed.

useEffect(() => {
    // Happens on mount

    return () => {
        // Optional; clean up before unmount
    };
}, []);

If you specify a dependency, the effect will only run when the dependency changes. We’ll get back to this in the watchers section.

const [count, setCount] = useState(0);

useEffect(() => {
    // Happens when `count` changes

    return () => {
        // Optional; clean up when `count` changed
    };
}, [count]);

Trying to directly translating lifecycle hooks to useEffect calls is generally a bad idea. It’s better to rethink things as a set of declarative side effects. When the effect is called is an implementation detail.

As Ryan Florence sums it up:

The question is not “when does this effect run” the question is “with which state does this effect synchronize with”

useEffect(fn) // all state
useEffect(fn, []) // no state
useEffect(fn, [these, states])

@ryanflorence on Twitter

Watchers

React alternative: The useEffect hook

Watchers are conceptually similar to lifecycle hooks: “When X happens, do Y”. Watchers don’t exist in React, but you can achieve the same with useEffect.

<!-- AjaxToggle.vue -->

<template>
  <input type="checkbox" v-model="checked" />
</template>

<script>
export default {
  data() {
    return {
      checked: false
    }
  },

  watch: {
    checked(checked) {
      syncWithServer(checked);
    }
  },

  methods: {
    syncWithServer(checked) {
      // ...
    }
  }
};
</script>
import { useEffect, useState } from 'react';

export default function AjaxToggle() {
  const [checked, setChecked] = useState(false);

  function syncWithServer(checked) {
    // ...
  }

  useEffect(() => {
    syncWithServer(checked);
  }, [checked]);

  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={() => setChecked(!checked)}
    />
  );
}

Note that useEffect will also run in the first render. This is the same as using the immediate parameter in a Vue watcher.

If you don’t want the effect to run on the first render, you’ll need to create a ref to store whether or not the first render has happened yet or not.

import { useEffect, useRef, useState } from 'react';

export default function AjaxToggle() {
  const [checked, setChecked] = useState(false);
  const firstRender = useRef(true);

  function syncWithServer(checked) {
    // ...
  }

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false;
      return;
    }
    syncWithServer(checked);
  }, [checked]);

  return (
    <input
      type="checkbox"
      checked={checked}
      onChange={() => setChecked(!checked)}
    />
  );
}

Slots & scoped slots

React alternative: JSX props or render props

If you render a template inside between a component’s opening and closing tags, React passes it as a children prop.

With Vue, you need to declare a <slot /> tag where inner contents belong. With React, you render the children prop.

<!-- RedParagraph.vue -->

<template>
  <p style="color: red">
    <slot />
  </p>
</template>
export default function RedParagraph({ children }) {
  return (
    <p style={{ color: 'red' }}>
      {children}
    </p>
  );
}

Since “slots” are just props in React, we don’t need to declare anything in our templates. We can just accept props with JSX, and render them where and when we want.

<!-- Layout.vue -->

<template>
  <div class="flex">
    <section class="w-1/3">
        <slot name="sidebar" />
    </section>
    <main class="flex-1">
        <slot />
    </main>
  </div>
</template>

<!-- In use: -->

<layout>
  <template #sidebar>
    <nav>...</nav>
  </template>
  <template #default>
    <post>...</post>
  </template>
</layout>
export default function RedParagraph({ sidebar, children }) {
  return (
    <div className="flex">
      <section className="w-1/3">
        {sidebar}
      </section>
      <main className="flex-1">
        {children}
      </main>
    </div>
  );
}

// In use:

return (
  <Layout sidebar={<nav>...</nav>}>
    <Post>...</Post>
  </Layout>
);

Vue has scoped slots to pass data to the slot that will be rendered. The key part of scoped slots is will be rendered.

Regular slots are rendered before they get passed to the parent component. The parent component then decides what to do with the rendered fragment.

Scoped slots can’t be rendered before the parent component, because they rely on data they’ll receive from the parent component. In a way, scoped slots are lazily evaluated slots.

Lazily evaluating something in JavaScript is rather straightforward: wrap it in a function and call it when needed. If you need a scoped slot with React, pass a function that will render a template when called.

For a scoped slots, we can once again use children, or any other prop for named scoped slots. However, we’ll pass down a function instead of declaring a template.

<!-- CurrentUser.vue -->

<template>
  <span>
    <slot :user="user" />
  </span>
</template>

<script>
export default {
  inject: ['user']
};
</script>

<!-- In use: -->

<template>
  <current-user>
    <template #default="{ user }">
      {{ user.firstName }}
    </template>
  </current-user>
</template>
import { useContext } from 'react';
import UserContext from './UserContext';

export default function CurrentUser({ children }) {
  const { user } = useContext(UserContext);

  return (
    <span>
      {children(user)}
    </span>
  );
}

// In use:

return (
  <CurrentUser>
    {user => user.firstName}
  </CurrentUser>
);

Provide / inject

React alternative: createContext and the useContext hook

Provide / inject allows a component to share state with its subtree. React has a similar feature called context.

<!-- MyProvider.vue -->

<template>
  <div><slot /></div>
</template>

<script>
export default {
  provide: {
    foo: 'bar'
  },
};
</script>

<!-- Must be rendered inside a MyProvider instance: -->

<template>
  <p>{{ foo }}</p>
</template>

<script>
export default {
  inject: ['foo']
};
</script>
import { createContext, useContext } from 'react';

const fooContext = createContext('foo');

function MyProvider({ children }) {
  return (
    <FooContext.Provider value="foo">
      {children}
    </FooContext.Provider>
  );
}

// Must be rendered inside a MyProvider instance:

function MyConsumer() {
  const foo = useContext(FooContext);

  return <p>{foo}</p>;
}

Custom directives

React alternative: Components

Directives don’t exist in React. However, most problems that directives solve can be solved with components instead.

<div v-tooltip="Hello!">
  <p>...</p>
</div>
return (
  <Tooltip text="Hello">
    <div>
      <p>...</p>
    </div>
  </Tooltip>
);

Transitions

React alternative: Third party libraries

React doesn’t have any built in transition utilities. If you’re looking for something similar to Vue, a library that doesn’t actually animates anything but orchestrates animations with classes, look into react-transition-group.

If you prefer a library that does more heavy lifting for you, look into Pose.