Vue vs. Svelte vs. React

Side by side comparison, built with Fauna

·

16 min read

Featured on Hashnode

For those of us who don’t get to work with frontend technologies, it can be hard to stay up to speed on the latest & greatest developments with JavaScript frameworks. For this reason, today we are going to explore two very popular frameworks, Vue and React, as well as a newer one I’ve heard great things about: Svelte.

In this post, I'm going to walk us through a side-by-side comparison of a Hello World Fauna app, built in each React, Vue, and Svelte. By running through the same core tasks and building the same end product, we will get to examine these tools in contrast.

In each, we’ll retrieve some data about Pokemon from Fauna and display it like this:

Image of a pokemon gallery

After building each, we’ll take a ten-thousand foot view of each framework’s developer experience and my observations on them as a Backend Engineer. Let’s get started!

Getting set up with Fauna

After playing with the Fauna GUI and CLI, I found the CLI a bit more to my taste, so that’s what I’ll be using here. You can get it on *nix machines with NodeJS/NPM installed by running npm i -g fauna-shell.

npm i -g fauna-shell

> faunadb@4.4.1 postinstall /usr/local/Cellar/node/17.3.0/lib/node_modules/fauna-shell/node_modules/faunadb
> node ./tools/printReleaseNotes

+ fauna-shell@0.13.0
added 325 packages from 242 contributors in 27.304s

If you are following along it is at this point that you'd want to head to Fauna's site and make an account. Quick heads up: if you’re going to use the CLI, don’t use an SSO option when you make your account. The CLI isn't set up to work with that authentication. I also built us some boilerplate code for a Vue project, so if you want to follow along clone the repo and start with origin/boilerplate.

Returning to the command line, let's get into our Vue project and create our database, first by logging in with the Fauna CLI. You’ll go through a process a bit like this:

➜ fauna cloud-login
? The endpoint alias prefix (to combine with a region): cloud
? The endpoint alias already exists. Overwrite? Yes
? How do you prefer to authenticate? Email and Password
? Email address: jtkaufman737@gmail.com
? Password: [hidden]
? Endpoints created. Would you like to set one of them as default? Keep 'cloud-us' endpoint as default
Endpoint 'cloud-us' set as default endpoint.

Then, let’s create our database and an API key. Keep your secret key private and be sure to mark it down somewhere as we will use it later.

➜  fauna create-database pokemon
creating database pokemon

  created database pokemon

  To start a shell with your new database, run:

  fauna shell pokemon

  Or, to create an application key for your database, run:

  fauna create-key pokemon

➜  fauna create-key pokemon
creating key for database 'pokemon' with role 'admin'

  created key for database 'pokemon' with role 'admin'.
  secret: xxxx-xxxxxx

  To access 'pokemon' with this key, create a client using
  the driver library for your language of choice using
  the above secret.

An important note before we move on

This is a very simple HelloWorld type exploration of three frameworks, and as such not how we would use any of these tools per se in the real world. We are going to let our JS code call the database directly, picking up credentials from local .env files.

As such, if you follow along you should not deploy this code, since the secret picked up will get built with the other static assets, making your secret visible if someone were to visit the web page.

If you do want to turn any of these demos into a larger project, tools like Netlify and Vercel have great options for how to deal with environment variables, or the database response could be returned through an API and server-side code.

Getting started with Vue

If you are familiar with modern single page apps, even if you haven’t worked with Vue, our project structure may seem somewhat familiar. You’ll notice that src/ contains the files we’d actively work on.

A typical Vue project can contain elements that have been removed from the boilerplate code we’ll use here, which was done to keep things extremely simple and make comparison between Vue, Svelte, and React easier and clearer.

With that in mind, our main active files are going to be App.vue and main.js. Let’s also add a data.json file to seed our database with some records.

// data.json 

{
    "id": 1,
    "name": "Bulbasaur",
    "imageUrl": "https://i.imgur.com/e7VtLbo.png"
}
{
    "id": 2,
    "name": "Pikachu",
    "imageUrl": "https://i.imgur.com/fmMERCo.png"
}
{
    "id": 3,
    "name": "Snorlax",
    "imageUrl": "https://i.imgur.com/TGf6qB8.png"
}
{
    "id": 4,
    "name": "Caterpie",
    "imageUrl": "https://i.imgur.com/A21Gpql.png"
}
{
    "id": 5,
    "name": "Jigglypuff",
    "imageUrl": "https://i.imgur.com/SU7yF1f.png"
}
{
    "id": 6,
    "name": "Abra",
    "imageUrl": "https://i.imgur.com/f59APqT.png"
}
{
    "id": 7,
    "name": "Weedle",
    "imageUrl": "https://i.imgur.com/XDeqSAB.png"
}
{
    "id": 8,
    "name": "Dratini",
    "imageUrl": "https://i.imgur.com/K9DxFvF.png"
}
{
    "id": 9,
    "name": "Charmander",
    "imageUrl": "https://i.imgur.com/KuZEzvo.png"
}

The Fauna CLI lets us easily import either JSON or CSV data to our new collection. Let’s import our new data.json:

➜  fauna import --path=./data.json --db=pokemon --collection=pokemon
Database 'pokemon' connection established
Start importing from ./data.json
Average record size is 113 bytes. Imports running in 10 parallel requests
9 documents imported from ./data.json to pokemon
 ›   Success: Import from ./data.json to pokemon completed

And we can confirm our records have made it up to the database by visiting our Fauna dashboard and drilling down into the right collection:

FaunaDB dashboard lists out our collection records

First, let’s connect our Vue app to the database using Fauna’s JavaScript driver. We’ll take the API secret that we wrote down from earlier and add it to a .env file - for Vue apps, anything prefixed with VUE_APP gets seamlessly picked up by the application so our .env file will look like this:

// .env 
VUE_APP_FAUNADB_SECRET=xxxx

We’ll then move over to main.js and run through our main logic. Here are the general steps:

  1. Import Fauna
  2. Use a constructor function to create a new database instance
  3. Bind that instance to the application
// main.js 

import Vue from 'vue'
import App from './App.vue'
import faunadb from 'faunadb'

// This constructor creates a new database instance, and supplying the secret
// authenticates us 
const db = new faunadb.Client({
    secret: process.env.VUE_APP_FAUNADB_SECRET, 
    domain: 'db.us.fauna.com',
})

Vue.config.productionTip = false
// binding $db and $q means our database instance and query commands 
// are easily accessible from here on out anywhere in our Vue code 
Vue.prototype.$db = db
Vue.prototype.$q = faunadb.query

new Vue({
  render: function (h) { return h(App) }
}).$mount('#app')

Here’s the fun part: Vue’s basic unit of functionality is a Single File Component - split into a <template> tag for our markup, a <script> tag containing our JavaScript functions and logic, and optionally a <style> tag for our CSS.

Let’s start with our <script>. We are going to deal with a few handy tools Vue gives us:

  • data is an object for storing easily accessible values local to our component. We will start it with an empty array and fill it with Pokemon from Fauna.
  • methods is an object whose properties can be functions for any behavior we want - here, we will call our database and get our records.
  • lifecycle methods are special events you can attach to behavior in Vue. I have arbitrarily chosen the moment our component is mounted to trigger the method call to Fauna. There are more lifecycle events than that, but let’s keep it simple today.
// App.vue 

<script>
export default {
  // data = our local component data, will be filled with  
  // pokemon later 
  data() {
    return {
      pokemon: []
    }
  },
  methods: {
    // our fetchPokemon method calls fauna, then updates  
    // data.pokemon 
    async fetchPokemon() {
      const q = this.$q 
      const response = await this.$db.query(
        q.Map(
          q.Paginate(q.Documents(q.Collection("pokemon"))),
          q.Lambda(item => q.Get(item))
        )
      ) 
      this.pokemon = response.data.map(item => {
        return item.data
      })
    }
  },
  // this lifecycle method is what kicks off the 
  // fetchPokemon() function running
  mounted() {
    this.fetchPokemon()
  }
}
</script>

My favorite part of Vue is the concise shorthand it offers to dynamically generate HTML based on component data. I’m going to make use of it here by leveraging a Vue construct called v-for to iterate over our array of Pokemon. It’ll look something like this:

// App.vue 

<template>
  <div id="app">
    <div class="home">
      <h3>Pokemon</h3>
      <section class="grid">
         <!-- 
             v-for iterates over pokemon array, :key loads the 
             javascript object value id, :src loads the
             Pokemon image url from each
             array item, so on and so forth 
          --> 
        <div class="card" v-for="p in pokemon" :key="p.id">
          <div class="header"><h6>{{ p.name }}</h6></div>
          <img 
             :src="p.imageUrl" 
             :alt="p.name"  
             class="pokemon"
           />
        </div>
      </section>
    </div>
  </div>
</template>

<script>
...

See the colon before a few of those HTML attributes? That lets Vue know that the attribute values aren’t strings, they’re dynamic JavaScript values based on the p variable we defined in the v-for shorthand. We also get to use those JavaScript values inside double brackets to interpolate into the content of the HTML tags — here that’s used to display the Pokemon names.

Last but not least, I hacked together some CSS that will be used (with minor edits) throughout each demo app. It’s not absolutely necessary, but we want these components to look good, so here it is:

// App.vue 
...

</script>

<style lang="scss">
#app {
  font-family: 'Inter', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
h1,h2,h3,h4,h5,h6 {
  font-family: acumin-pro,sans-serif;
  font-weight: 500;
  font-style: normal;
}
h3 {
  font-size:2em;
}
h6 {
  font-size:1.25em;
}
div.card {
  border-radius:30px;
  border:.25px lightgray solid;
  margin:5px;
}
div.header {
  width:100%;
  color:white;
  background: linear-gradient(90deg,#3F00A5 0%,#813EEF 100%);
  display:flex;
  justify-content:space-around;
  align-items:center;
  border-top-left-radius:30px;
  border-top-right-radius:30px;
  height:60px;
}
a {
  color:#3F00A5;
}
a:hover {
  color:#813EEF;
}
div.home {
  width:60%;
  margin:auto;
}
section.grid {
  display:grid;
  grid-template-columns:33.33% 33.33% 33.33%;
  grid-template-rows:auto;
  margin:auto;
  padding:5% 20% 5% 20%;
}
img.pokemon {
  height:100px;
  width:100px;
  padding:25px 10px;
}
</style>

With all that in place, let’s return to the command line and kick off a local build:

➜  npm run serve

> vue-faunadb@0.1.0 serve /Users/jkaufman/Code/vue-faunadb
> vue-cli-service serveDONE  Compiled successfully in 97ms                                                                                            11:30:06 AM

  App running at:
  - Local:   http://localhost:8082/ 
  - Network: http://192.168.58.105:8082/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

Voila! if you return to localhost:8080 you will be able to see our Vue app displaying the Pokemon gallery shown earlier in all its glory. With that out of the way, let’s do the same with some other tools!

Recreating the app in React

React is the most popular web framework out there (or so it has seemed to me for a while), so it’s much more likely you’re familiar with how it works. Let’s try to reconstruct this application and see the differences between Vue and React. If you’re following along, pop over to this repo and grab the latest from origin/boilerplate.

For those familiar with React, you will again notice that I’ve stripped out a lot - some of the files are outside the scope of this article and would just cloud the fundamental comparison of these frameworks.

We’ll run through the same logic, although this time in index.js:

  1. Import Fauna
  2. Use a constructor function to create a new database instance
  3. Bind that instance to the application
// index.js 
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import faunadb from 'faunadb';

// create database connection/instance through constructor 
const db = new faunadb.Client({
  secret: process.env.REACT_APP_FAUNADB_SECRET, 
  domain: 'db.us.fauna.com',
});

ReactDOM.render(
  <React.StrictMode>
    <App 
        // For react, we're going to bind the database instance 
        // and query functionality as props 
      db={db}
      q={faunadb.query} 
    />
  </React.StrictMode>,
  document.getElementById('root')
);

In our new React app, let’s go to .env file and add our Fauna secret — this time using a React specific prefix:

// .env 
REACT_APP_FAUNADB_SECRET=xxxxx

Our last piece of business here will be some work in App.js. We will need to:

  • Create methods to call Fauna and access our list of Pokemon.
  • Iterate over the data and display it through dynamically generated markup.
  • We will use useEffect and useState instead of lifecycle methods to trigger our API call. This is because React doesn’t have lifecycle methods, which is something I miss coming from Vue — more on that later.
// App.js

import './App.css'
import { useEffect, useState } from 'react'

function App(props) {
  const [pokemon, setPokemon] = useState([])
  const q = props.q

  // use effect means this will fire on render
  useEffect(() => {
    // this function uses our props for the database and Fauna query capabilities
    async function fetchPokemon() {
      const response = await props.db.query(
        q.Map(
          q.Paginate(q.Documents(q.Collection("pokemon"))),
          q.Lambda(item => q.Get(item))
        )
      )

      let updatedPokemon = response.data.map(item => {
          return item.data
      })

      setPokemon(updatedPokemon)
    }

    fetchPokemon()
  }, [])


  return (
    <div className="App">
      <div className="home">
        <h3>Pokemon</h3>
        <section className="grid">
          {
            // without v-for like in Vue, we instead use vanilla JS 
            // to get the iteration over our data 
            pokemon.map(p => {
              return (
                // all dynamic JS values in React are expressed as {x}
                <div className="card" key={p.id}>
                  <div class="header"><h6>{p.name}</h6></div>
                  <img src={p.imageUrl} alt={p.name} className="pokemon"/>
                </div>
              )
            })
          }
        </section>
      </div>
    </div>
  );
}

export default App;

Let it rip with an npm run start, and you should see a perfect replica of the app we created with Vue running on localhost:3000. Again, a pretty seamless experience to get a simple application prototype running.

Last but not least, we are going to do the same again, with one final tool.

Recreating the app in Svelte

I was honestly very excited for this one because I have been primarily a backend developer for quite a while, and as a result, I’ve had fewer opportunities than I’d like to play with the newest JavaScript stuff. Svelte has sounded interesting for a long time and I was happy to finally get to give it a whirl.

As before, grab the boilerplate code and checkout to origin/boilerplate if you’re following along. You know the drill at this point: step one is sticking Fauna in our entry point JS file (which is main.js here). Here’s how we tackle it in Svelte:

// main.js

import App from './App.svelte';
import faunadb from 'faunadb';

// create db instance through constructor, secret for authentication
const db = new faunadb.Client({
    secret: process.env.SVELTE_APP_FAUNADB_SECRET, 
    domain: 'db.us.fauna.com',
});

const app = new App({
    target: document.body,
    // to make the db and query functionality available widely, we are 
    // going to pass them as props in the main application instance
    props: {
        db: db,
        q: faunadb.query
    }
});

export default app;

You’ll want to remember to make an equivalent .env file here too of course.

// .env 

SVELTE_APP_FAUNADB_SECRET=xxxx

The main App.svelte file reminds me a lot of Vue, sectioned out for different functional areas by default. Take a look:

// App.svelte 
<script></script>

<main id="app"></main>

<style></style>

Here’s where it gets interesting. Svelte, like Vue, supports special shorthand iteration operations in its markup. For Svelte, these are denoted both with characters and keywords — for instance, {#each /} will allow us to iterate over an array. We also get lifecycle methods back, and can tie our API call to component mounting. The expression of {#each /} to me was particularly fascinating. It reminds me — visually speaking — more of templating methods in Rails or Django than the equivalent version of this functionality in React or Vue. There’s nothing wrong with that; it feels natural to me.

In App.svelte, next we’ll register the “on mount” behavior, a method containing our database call, and the iteration and display of the results in markup.

// App.svelte 

<script>
    import { onMount } from "svelte"; // Yay lifecycle methods! 

    let pokemon = [];
    // the double $$ is also visually interesting, maybe not what I'd expect
    // to signify accessing props, but nothing wrong with it either 
    const db = $$props.db; 
    const q = $$props.q;

    // method to grab our pokemon records 
    onMount(async () => {
        const response = await db.query(
            q.Map(
                q.Paginate(q.Documents(q.Collection("pokemon"))),
                q.Lambda(item => q.Get(item))
            )
        ) 
        pokemon = response.data.map(item => {
            return item.data
        })
    })
</script>

<main id="app">
    <div class="home">
        <h3>Pokemon</h3>
        <section class="grid">
         <!-- here starts our interesting loop block syntax --> 
           {#each pokemon as p}
              <div class="card">
              <!-- Like React, dynamic values grabbed and expressed with {x} --> 
                  <div class="header"><h6>{p.name}</h6></div>
                  <img src={p.imageUrl} alt={p.name} class="pokemon"/>
              </div>
           {/each}
        </section>
    </div>
</main>

<style>
...

At this point, we can put the pedal to the medal with a npm run dev. If you look at localhost:5000 you should again see a perfect replica of our Pokemon gallery, as pictured in the intro.

Comparison of these tools

As someone who leans towards the backend but thoroughly understands frontend concepts, I think I have more of an objective wide-lens view of these JavaScript tools, which led to some interesting observations:

  • All of these tools are simple enough for a backend developer like myself to jump in without much trouble. For example, I recall finding the class-based syntax React had at one point confusing to wade through. Being able to return to React and use functional components was great
  • Svelte made a strong first impression on me as a long time Vue user. I liked it but could also imagine someone coming from React liking it. Having never touched it before, getting the markup and methods working took pretty much zero time for a rookie.
  • The only area in which Svelte felt weak was in build configuration. Unlike Vue and React which by default utilize Webpack to build, bundle, and minimize code, Svelte uses another tool: Rollup. Although I saved you from this experience in the tutorial, dealing with Rollup presented multiple hiccups that reminded me of the pre-Webpack-3 days of working with Single Page Apps, when extensive Webpack configuration was sometimes required. For anyone interested you can read more about these two tools here
  • On a related note, I did feel like the amount of time it took to figure out the standard method of passing Svelte environment variables felt surprisingly long. I would consider that an area to be improved upon - if a .env file with SVELTE_APP_VAR was plug and play (as it is for React or Vue) I would have been a much happier developer
  • Both Svelte and React are by default stricter — I forgot that it is normal for your JavaScript build to scream at you about unused CSS classes. This is probably a positive, especially if you’re going to be building something for production in the long run
  • Plenty of people prefer React because of fewer framework-isms, like Vue’s v-for or Svelte’s {#each /each} for dynamically generated markup. For me, they are intuitive enough that I like having the option, but I can see how they’d be confusing for many
  • I also like Vue and Svelte’s component lifecycle methods. I find their names (mounted, onMounted) more intuitive than useEffect in React. If I were new to JavaScript, I might expect something called useEffect to be related to DOM behavior or something else
  • If I had to make a decision for a personal project after this experience, I’d rank my favorites as Svelte first, then Vue, and lastly React. Svelte just gave me a warm and fuzzy feeling that puts it over the top for me, especially with all its potential

The elephant in the room, of course, is that Svelte, uniquely, does not utilize a virtual DOM. In their own words, Svelte cites the rationale for this as a way to get away from the overhead required to sync and compare the actual and virtual DOM. As someone with a utilitarian need for JS frameworks, I didn’t mind the virtual DOM when working with it, nor could I particularly think of ways I missed it playing with Svelte. Truthfully, that conversation doesn’t seem relevant until we’re building much larger apps with much more at stake if our performance starts to dip. I do find Svelte’s argument compelling, though, so I’ll definitely be following the large scale adoption of Svelte.

Wrapping Up

I enjoyed building all three demo apps. React once felt very intimidating to me, but the move away from Redux and class based components has made it feel more intuitive.

Vue is the only one of these technologies I have worked with significantly, ever since 2018. I continue to like it as a “mellow” tool, and it is a recurring comment I hear from other backend folks that Vue feels approachable to us.

Svelte lived up to the hype, at least in this very minimal example! Although I have been a Vue person for years, I would strongly consider using Svelte instead on my next personal project based on my initial positive experience and wanting to learn more.