Petal
How to decide between React and Vue?

How to decide between React and Vue? Gatsby and Gridsome?

Everywhere you look, you see the word “Jamstack.” So you’ve probably thought about building a site using a static site generator. But where do you start? How do you choose the right framework for you and your team?


Ondrej PolesnyPublished on May 18, 2020

In this article, I’ll compare two leading JS frameworks—React and Vue.js—from many perspectives, including learning curve, performance, the size of communities, and interest from employers.

I’ll also explain the differences between building a static site using their respective static site generators, Gatsby and Gridsome.

Is Jamstack always JavaScript?

If you’re new to the Jamstack, just know that it’s not a product or a framework, but a way of building websites. Jamstack stands for JavaScript, API, and Markup. The key aspect of a Jamstack site is that it does not depend on a web server. Static sites built with JavaScript frameworks are Jamstack, but so are static sites generated using .NET, PHP, and other platforms. The JavaScript in the name of the stack points to the dynamic capabilities of static sites once deployed.

JavaScript is the most popular platform for the Jamstack because of its ease of use. You can get a website up and running in hours. And almost everyone knows a bit of JavaScript, which makes the entry barrier for beginners low.

React or Vue.js

React, Vue.js, and Angular form the big trio of the most popular JavaScript frameworks. For the purposes of this article, I’m focusing on React and Vue.js because the learning curve for Angular is steep, and it is generally a better fit for large projects and teams who prefer TypeScript and object-oriented programming.

Let’s see how React compares to Vue.js and how many points they can get in each of these categories:

Learning curve

To start with React, you’ll need to understand JSX, build systems, and have previous JavaScript knowledge. Vue.js does not require any of those, only a bit of JavaScript knowledge to understand syntax.

Winner: Vue.js

Documentation

Vue.js’s documentation is simpler and clearer than React’s.

Winner: Vue.js

Performance

Both React and Vue.js have similar performance. Vue.js is about half the size. However, compared to other website resources, this fact alone should not be a deciding factor.

Winner: tie

Community size

Both React and Vue.js have large communities with similar numbers of stars, contributors, and watchers of their GitHub repos.

Winner: tie

Scaling

Both are prepared to scale up. Vue.js is better for scaling down.

Winner: tie

Interest from employers

While there are many job offers for both, React is globally experiencing higher interest from employers.

Winner: React

You see there’s no clear winner, it’s 5:4 in favor of Vue.js, but it’s so close that I’d call this a tie. You’ll find developers passionate about React as well as Vue.js. Many people like and use both. It’s like cars. If you learn how to drive an Audi, you’ll be able to drive a Mercedes too.

Gatsby vs. Gridsome match

Both Gatsby and Gridsome are static site generators for React and Vue.js respectively. I’m sure you’ve heard of the first one sometime somewhere. Gatsby has become the king of SSGs as it’s been around for a long time and targets React developers.

Vue.js is younger than React, and so Gridsome is also younger than Gatsby. Gatsby’s head start has given it a decisive advantage in the feature battle against Gridsome. But how do they compare when we start building the same website using them?

Kontent.ai

This is Phantom, a free HTML web template I got from HTML5UP.net. It contains a list of articles on the main page, and each of them also has its dedicated page. A perfect simple use-case for a static site.

The content I’ll be using comes from a headless CMS Kentico Kontent that I use for my personal site. However, these days I publish most of my blog posts externally, so don’t be surprised if you see some older pieces.

Kontent.ai

Initializing a blank site

The first step towards building a new site using any static site generator is to get a starter site. There are usually many starters to choose from, and the best bet is to start with the one that features your preferred way of getting content. In my case, the source of content is a headless CMS.

Gatsby

A short Google search of “Gatsby Kontent starter” takes me to the GitHub repository of the starter. I need to clone this repository and install packages:

git clone https://github.com/Kentico/gatsby-starter-kontent
cd gatsby-starter-kontent
npm i

By running gatsby develop, I can access the site in a web browser.

Gridsome

Just like with Gatsby, there is also a Kontent-specific sample site, but it’s full-featured. I’d rather use a blank Gridsome installation:

npm i --global @gridsome/cli
gridsome create gridsome-starter-kontent
cd gridsome-starter-kontent

By running gridsome develop, a nice Hello, world! site displays.

Adding the source plugin

I’m using data from a headless CMS delivered by REST API. The static site generator needs a plugin that downloads the data and converts them into local GraphQL nodes.

Gatsby

I installed a starter site that already contains the Kentico Kontent source plugin. I only need to change the projectId in gatsby-config.js:

918ba95f-885b-0045-a463-650b22e1196a

When I try to run the site now using gatsby develop, it will fail with a few GraphQL errors. That is expected, I changed the projectId, and now the site receives a completely different set of data than its pages require. We’ll change that later.

Gridsome

As I installed a blank Gridsome project, I need to install the Kontent plugin using npm:

npm install @meeg/gridsome-source-kentico-kontent

I also need to add the plugin including my projectId to the gridsome.config.js:

plugins: [
 {
   use: '@meeg/gridsome-source-kentico-kontent',
   options: {
     deliveryClientConfig: {
       projectId: process.env.KENTICO_KONTENT_PROJECT_ID
     }
   }
 }
]

The site will still work in the same way as its index page is not bound to any data source.

Adding styles and images

The original web template, Phantom, contains a few stylesheets and images. Let’s take a look at how we can add them.

Gatsby

For the frontend assets, at the root of the project, I can create a folder and name it static. Gatsby will simply copy all the contents of that folder into the final build.

For stylesheets, the best is to import them into the components and pages directly. So the contents of the css folder of the Phantom template will go into src/css.

Having all stylesheets in place, I need to change which styles are imported into the layout of the Gatsby site. The layout is defined in src/components/layout.js and contains the following line that I should remove:

import './layout.css';

And I need to add these lines instead:

import '../css/fontawesome-all.min.css';
import '../css/main.css';

The last step is to adjust the path of referenced assets within CSS files. Those are typically fonts and background images. In my case it’s font-awesome:

...
src:url(../webfonts/fa-brands-400.eot);
...

The font files will be copied to the final build automatically, so their path will change accordingly:

...
src:url(/assets/webfonts/fa-brands-400.eot);
...

I run Replace all "../webfonts/" to "/assets/webfonts/".

Gridsome

The folder static for static assets is already in the project root, so I can copy-paste the assets directly.

To make sure Gridsome adds the styles to the layout, I need to add the following code into the special file main.js:

head.link.push({
    rel: 'stylesheet',
    href: '/assets/css/fontawesome-all.min.css'
  })

head.link.push({
   rel: 'stylesheet',
   href: '/assets/css/main.css'
 })

As both styles and fonts are placed in the same static folder, I don’t need to update the path to font files.

Adjusting the layout

Once the styles are in place, I need to adjust the HTML of the layout and content pages. The layout is the part of the page that is the same for all pages.

Kontent.ai

The way I usually create the layout is to copy and paste the HTML code between <body> and </body> tags from the template. Then I remove the code where each page should render its content.

Gatsby

The default layout is in src/components/layout.js. We already stumbled upon this file when importing CSS.

I replace all the code within StaticQuery’s render part with the HTML from the template:

<StaticQuery
    query={...}
    render={(data) => (*HTML code from the template*)}
></StaticQuery>

Gatsby requires JSX, but I pasted HTML, so I need to make some changes. The most important one is to change all class="*" attributes to className="*". The HTML comments also need to be removed.

The Phantom template has a <div id="wrapper"> element that wraps around the whole page but contains a few script imports at the end. Gatsby requires a single root element, so I have to use another wrapping element, <div> or <React.Fragment>.

Then I need to replace the HTML where child pages will render their content with {children}. In this case, it’s the code between <div id="main"> and </div> just before <footer>. If you’re following my steps, don’t just delete the code, put it into the clipboard as you’ll need it for the next step.

Gridsome

The default layout is in src/layouts/Default.vue. It’s a Vue.js component, and as such has three parts:

  • HTML markup within <template> and </template> tags
  • Functionality within <script> and </script> tags
  • GraphQL data queries within <static-query> and </static-query> tags (or page-query in page context)

In the layout, I only need markup, so I copy-paste the HTML from the template to the <template> section. The middle section between <div id="main"> and </div> just before <footer> needs to be replaced, just like with Gatsby, with a <slot /> tag.

Adding static pages

The only truly static page in the Phantom template is the index. It contains a list of blog posts with links to their URLs.

Gatsby

The GraphQL query for getting the blog post data using the Kontent source plugin looks like this:

allKontentItemBlogPost {
  edges {
    node {
      system {
        codename
      }
      elements {
        title {
          value
        }
        teaser {
          value
        }
        image_url {
          value
        }
        image {
          value {
            url
          }
        }
      }
    }
  }

Note the codename; I’ll use that as the blog post URL.

The code of the index page is in the src/pages/index.js file. There is a constant query that holds the GraphQL query. The code of the page is in the constant Index, which is also the default export. The HTML code I removed in the previous section goes here in the return expression.

Now, I need to dynamically generate the markup between <section class="tiles"> and </section>. JSX allows us to combine JavaScript and HTML, so I use the map function to do so:

const items = data.allKontentItemBlogPost.edges.map(({node:item}, index) => 
    <article className={`style${index}`}>
      <span className="image">
        <img src="images/pic01.jpg" alt="" />
      </span>
      <a href={`/blog/${item.system.codename}`}>
        <h2>{item.elements.title.value}</h2>
        <div className="content" dangerouslySetInnerHTML={{__html: item.elements.teaser.value}}>
        </div>
      </a>
    </article>
)

The <article> requires className that increments (style1, style2, style3), so I also use index variable of the mapping function to properly generate the CSS. The target link of every blog post is composed as /blog/{codename}.

Gridsome

The Index page is implemented in src/pages/Index.vue and contains two sections—template and script. For my use case, I don’t need the <script> part, but I need to add a page-query section with the GraphQL query that gathers data from the headless CMS:

<page-query>
query {
  allBlogPost {
    edges {
      node {
        path
        imageUrl
        image {
          URL
        }
        teaser
        title
      }
    }
  }
}
</page-query>

You see the structure of data is a bit different compared to Gatsby. It’s because every source plugin treats the data differently.

The markup within <template> tags is identical to the Phantom template except for the section where I render the list of blog posts. Vue.js allows me to specify the HTML and use bound attributes to generate as many iterations as I have blog posts.

<section class="tiles">
  <article v-for="(item, index) in $page.allBlogPost.edges" :key="item.node.codename" :class="`style${index}`">
    <span class="image">
      <img src="images/pic01.jpg" alt="" />
    </span>
    <a :href="`${item.node.path}`">
      <h2>{{ item.node.title }}</h2>
      <div class="content" v-html="item.node.teaser">
      </div>
    </a>
  </article>
</section>

Did you notice the item.node.path in the link? Shouldn’t there be /blog/{item.node.codename}? Good job! I’ll explain that in the next section where we actually generate the pages.

Generating static pages based on content

This is the most interesting part where I need to generate pages based on a template and data set coming from the headless CMS. I also need to specify the URLs of such pages.

Gatsby

The template I’ll use for generating the pages has the same structure as the previously edited page. Therefore, I copy-pasted the index.js file into templates folder and renamed it to blog.js.

Kontent.ai

The template receives a codename of the item it should render. It needs to implement its own GraphQL query to get additional data like header, images, and content:

query projectReferenceQuery($codename: String!) {
      kontentItemBlogPost(system: {codename: {eq: $codename}}) {
        elements {
          title {
            value
          }
          image_url {
            value
          }
          teaser {
            value
          }
          image {
            value {
              url
            }
          }
        }
        system {
          codename
        }
      }
  }

Now there is only a bit of additional logic and markup left:

const item = data.kontentItemBlogPost;

let imageUrl = item.elements.imageUrl;
if (!imageUrl && item.elements.image.value.length > 0)
{
  imageUrl = item.elements.image.value[0].url;
}

return (
    <Layout>
      <div id="main">
        <div class="inner">
          <h1>{item.elements.title.value}</h1>
          <span class="image main"><img src="images/pic13.jpg" alt="" /></span>
          <div dangerouslySetInnerHTML={{__html: item.elements.teaser.value}}></div>
        </div>
      </div>
    </Layout>
);

Once the template is ready, I need to add a bit of code into gatsby-node.js file, which takes care of getting the codenames and generating the pages. In the GraphQL query, I am getting codenames of all blog posts and later invoking createPage function that specifies on which path and using which template should Gatsby generate each page.

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions;
  return new Promise((resolve) => {
    graphql(`
    {
      allKontentItemBlogPost {
        edges {
          node {
            system {
              codename
            }
          }
        }
      }
    }
    `).then(result => {
        result.data.allKontentItemBlogPost.edges.forEach(({node}) => {
          createPage({
            path: `blog/${node.system.codename}`,
            component: path.resolve(`./src/templates/blog.js`),
            context: {
              // Data passed to context is available in page queries as GraphQL variables.
              codename: node.system.codename
            },
          });
        })
        resolve();
    });
  });
};

Gridsome

The templates are created in the same way as pages. Gridsome works with the concept of IDs. Every content item has a unique identifier you can use to query for it.

Kontent.ai

Therefore, the template I’m about to build starts with a page-query that gets the data of a specific blog post:

<page-query>
query MyQuery ($id:ID!) {
  blogPost (id: $id)
  {
    codename
    imageUrl
    image {
      url
    }
    teaser
    title
  }
}
</page-query>

The markup of the template looks like this:

<template>
  <Layout>
    <div id="main">
      <div class="inner">
        <h1>{{$page.blogPost.title}}</h1>
        <span class="image main"><img :src="$page.blogPost.imageUrl ? $page.blogPost.imageUrl : $page.blogPost.image.value[0].url" alt="" /></span>
        <div v-html="$page.blogPost.teaser"></div>
      </div>
    </div>
  </Layout>
</template>

Now the real magic comes. Gridsome, unlike Gatsby, meets you halfway and does not require you to build the page-creating logic all by yourself. The content type I just created a template for is called BlogPost. Gridsome knows that, so as long as the template is named in the same way I only need to specify the URL structure in the gridsome.config.js file:

module.exports = {
  ...
  templates: {
    BlogPost: '/blog/:codename'
  }
}

By specifying the URL, Gridsome will enhance all BlogPost content items with a path variable that is shortened and adjusted to meet the URL requirements. You should use it for all linking within the project. If you used /blog/{codename} in your code, you’d be referencing not existing pages.

Who’s the winner?

In this article, I tried to show you how similar it is to build a static site using different frameworks. Blah blah, just tell me which one is better already?!

I personally like Vue.js and Gridsome better. Both have a special place in my heart for being so intuitive and straightforward. I love their documentation. It helped me a lot when I was starting with static site generators.

Gatsby, on the other hand, wins the feature battle, and I use it more for clients’ projects that require advanced functionality such as image preloading, multiple languages, complicated forms, and so on. The source plugin getting data from the headless CMS Kentico Kontent for Gatsby is also more advanced.

I wouldn’t say one is better than the other. Both are great. If you need to pick one, try to build a sample site on both of them, and trust your gut.

Written by

Ondrej Polesny

As Developer Evangelist, I specialize in the impossible. When something is too complicated, requires too much effort, or is just simply not possible, just give me a few days and we will see :).

More articles from Ondrej

Feeling like your brand’s content is getting lost in the noise?

Listen to our new podcast for practical tips, tricks, and strategies to make your content shine. From AI’s magic touch to content management mastery and customer experience secrets, we’ll cover it all.

Listen now
Kontent Waves