DEV Community

loading...
Cover image for Random Quote App - Angular vs. React vs. Vue Comparison

Random Quote App - Angular vs. React vs. Vue Comparison

corscheid profile image Corey Scheideman Updated on ・13 min read

Originally posted on corscheid.dev

In the last post, we looked at how to build a simple random quote app in Angular. In this post, we will compare the same app in React and Vue, to have a look at the similarities and differences in Angular, React, and Vue. We'll cover the project structure, and tooling of each of these technologies, as well as how components are implemented in each.

Contents

Links

All three versions of the random quotes app are available on my GitHub.

Following are some Documentation links if you want to check these technologies out in more detail or for how to get a project started with each, etc.

Tooling

Each of these three has an associated command line tool that can be used to generate an initial project with all the boilerplate code for getting everything up and running quickly. For Angular, this is the ng command (Angular CLI), for React it's create-react-app, and for Vue, it's the vue command (Vue CLI). Here's a quick rundown with some examples.

Creating a new project called my-app:

# Angular
ng new my-app

# React
npx create-react-app my-app

# Vue
vue create my-app
Enter fullscreen mode Exit fullscreen mode

Documentation links for these tools:

Project Structure

Angular seems to create a lot more files and directories by default when generating a project using ng new than React with create-react-app or Vue with vue create. Vue creates the lowest number of files and directories.

Note: The following project file trees displayed are all excluding the node_modules/ and .git/ directories, for sake of brevity.

Angular

Upon generating a new project with the Angular CLI (ng new), the following tree of directories and files is created.

fcc-random-quote-machine-angular
├── .browserslistrc
├── .editorconfig
├── .gitignore
├── README.md
├── angular.json
├── e2e/
│   ├── protractor.conf.js
│   ├── src/
│   │   ├── app.e2e-spec.ts
│   │   └── app.po.ts
│   └── tsconfig.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── src/
│   ├── app/
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.component.ts
│   │   └── app.module.ts
│   ├── assets/
│   │   └── .gitkeep
│   ├── environments/
│   │   ├── environment.prod.ts
│   │   └── environment.ts
│   ├── favicon.ico
│   ├── index.html
│   ├── main.ts
│   ├── polyfills.ts
│   ├── styles.scss
│   └── test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json

6 directories, 30 files
Enter fullscreen mode Exit fullscreen mode

React

Create React App generates the following.

fcc-random-quote-machine-react
├── .gitignore
├── README.md
├── package.json
├── public/
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src/
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   ├── logo.svg
│   ├── reportWebVitals.js
│   └── setupTests.js
└── yarn.lock

2 directories, 18 files
Enter fullscreen mode Exit fullscreen mode

Vue

And Vue CLI (vue create) generates the following.

fcc-random-quote-machine-vue
├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── README.md
├── babel.config.js
├── package.json
├── public/
│   ├── favicon.ico
│   └── index.html
├── src/
│   ├── App.vue
│   ├── assets/
│   │   └── logo.png
│   ├── components/
│   │   └── HelloWorld.vue
│   └── main.js
└── yarn.lock

4 directories, 13 files
Enter fullscreen mode Exit fullscreen mode

Components

Angular, React, and Vue are all component-based. The UI of an app is typically broken down into smaller components.

Layouts

Within a component, there is generally some sort of layout information associated, as to how it should be displayed in the browser. As you'll see in the following sections, Angular and Vue use HTML templates, and React uses either functions that return JSX or classes with a render() method that returns JSX. JSX is a sort of XML-in-JavaScript syntax; you can read more about JSX on the React Documentation Page. Essentially it allows the use of HTML tag-like syntax inside of JavaScript in order to make a readable template without needing to use a separate HTML file or interpolated template string.

App Layout

Here we'll take a look at the main layout structure in each. You'll notice that they are very similar. Comparing things at this level makes the difference between these technologies appear to be mostly that of syntax.

For example, in Angular, interpolating variable values from the TypeScript is done with "mustache" double braces {{ }}, and Vue does it the same way, but with React, which is typically written with JSX, we see single braces { }.

Event handler binding in Angular such as onClick is written with (click)="", where the JavaScript expression executed on the click is placed in the quotes. In Vue, it's the same idea with @click="", which is shorthand for v-on:click="". In React, it's onClick={}, which is a prop passed down to the component and the JS expression is placed between the single braces.

Binding is how HTML element attributes and the corresponding component class variables are kept in sync with each other when a change happens in either direction. In Angular, an example for the syntax for this would be [tweetURL]="tweetURL" (as seen in the following code snippets). Square brackets are used around the attribute name to signify that it is to be bound to a variable of the associated class, and in the quotes goes the variable it is bound to. In Vue, we have the same idea going on with :tweetURL="tweetURL", which is short for v-bind:tweetURL="tweetURL". These are somewhat similar to how React passes props down to child components, with the tweetURL={tweetURL} attribute-like syntax in JSX, but work differently under the hood.

Angular

<!-- src/app/app.component.html (Angular) -->
<div *ngIf="loading; else content" id="loading">
  <h1>loading...</h1>
</div>
<ng-template #content>
  <app-quote-box
    [author]="quote.author"
    [quote]="quote.quote"
    [tweetURL]="tweetURL"
    [getNewQuote]="getNewQuote"
  ></app-quote-box>
</ng-template>
Enter fullscreen mode Exit fullscreen mode

React

// src/App.js – App function return statement (React)
return loading ? (
  <div id="loading">
    <h1>loading...</h1>
  </div>
) : (
  <QuoteBox
    quote={quote.quote}
    author={quote.author}
    getNewQuote={getNewQuote}
    tweetURL={tweetURL}
  />
);
Enter fullscreen mode Exit fullscreen mode

Vue

<!-- src/App.vue – template section (Vue) -->
<template>
  <div id="app">
    <div v-if="loading" id="loading">
      <h1>loading...</h1>
    </div>
    <QuoteBox
      v-else
      :quote="quote.quote"
      :author="quote.author"
      :tweetURL="tweetURL"
      :getNewQuote="getNewQuote"
    ></QuoteBox>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

QuoteBox Layout

Again, everything is almost the same, except some bits of syntax.

Angular

<!-- `src/app/quote-box/quote-box.component.html` (Angular) -->
<div id="quote-box">
  <h1 id="text"><i class="fa fa-quote-left"></i> {{ quote }}</h1>
  <p id="author">- {{ author }}</p>
  <div class="btn-row">
    <button class="btn btn-primary" id="new-quote" (click)="getNewQuote()">
      New quote
    </button>
    <a
      id="tweet-quote"
      href="{{ tweetURL }}"
      target="_top"
      class="btn btn-secondary"
    >
      <i class="fa fa-twitter"></i> Tweet
    </a>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

React

// src/components/QuoteBox.js – QuoteBox function return statement (React)
return (
  <div id="quote-box">
    <h1 id="text">
      <i className="fa fa-quote-left"></i> {props.quote}
    </h1>
    <p id="author">- {props.author}</p>
    <div className="btn-row">
      <button
        className="btn btn-primary"
        id="new-quote"
        onClick={props.getNewQuote}
      >
        New quote
      </button>
      <a
        id="tweet-quote"
        href={props.tweetURL}
        target="_top"
        className="btn btn-secondary"
      >
        <i className="fa fa-twitter"></i> Tweet
      </a>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Vue

<!-- src/components/QuoteBox.vue – template section (Vue) -->
<template>
  <div id="quote-box">
    <h1 id="text"><i class="fa fa-quote-left"></i> {{ quote }}</h1>
    <p id="author">- {{ author }}</p>
    <div class="btn-row">
      <button class="btn btn-primary" id="new-quote" @click="getNewQuote()">
        New quote
      </button>
      <a
        id="tweet-quote"
        href="tweetURL"
        target="_top"
        class="btn btn-secondary"
      >
        <i class="fa fa-twitter"></i> Tweet
      </a>
    </div>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Styles

The same Sass styles were used in each version of this app. The only differences that occur are in the mechanisms for how global styles and component-specific styles are applied.

Global Styles

The global sass stylesheet is the same in all three, except that the filepaths / filenames differ.

Angular, React, and Vue

/* src/styles.scss (Angular) */
/* src/index.scss (React) */
/* src/styles/styles.scss (Vue) */

/* Bootstrap 5 */
@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css");
/* Font Awesome */
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css");
/* Google Fonts */
@import url("https://fonts.googleapis.com/css2?family=Amiri&family=Indie+Flower&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Neucha&display=swap");

$blue: #58f;

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: $blue;
  height: 100%;
  overflow-y: hidden;
}
Enter fullscreen mode Exit fullscreen mode

App Styles

Here are the styles for the main app component.

Angular & React

/* src/app/app.component.scss (Angular) */
/* src/App.scss (React) */
$white: #fafafa;

#loading {
  color: $white;
  font-family: "Amiri", serif;
}
Enter fullscreen mode Exit fullscreen mode

Vue

In Vue, styles go inside a style section at the bottom of the component file.

<!-- src/App.vue – style section (Vue) -->
<style lang="scss">
  $white: #fafafa;

  #loading {
    color: $white;
    font-family: "Amiri", serif;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

QuoteBox Styles

Here are the styles for the QuoteBox component.

Angular & React

/* src/app/quote-box/quote-box.component.scss (Angular) */
/* src/components/QuoteBox.scss (React) */
$black: #3f3f3f;
$white: #fafafa;

#quote-box {
  padding: 2em;
  background-color: $white;
  margin: 20%;
  border-radius: 10px;
  color: $black;

  #text {
    font-family: "Amiri", serif;
  }
  #author {
    font-family: "Neucha", cursive;
    font-size: 2.5em;
  }
  .btn-row {
    display: flex;
    flex-direction: row;
    justify-content: flex-end;

    #tweet-quote {
      margin-left: 1em;
    }
  }
}

@media only screen and (max-width: 480px) {
  #quote-box {
    margin: 0;
    overflow-y: auto;
  }
}
Enter fullscreen mode Exit fullscreen mode

Vue

<!-- src/components/QuoteBox.vue – style section (Vue) -->
<style lang="scss" scoped>
  $white: #fafafa;
  $black: #3f3f3f;
  #quote-box {
    padding: 2em;
    background-color: $white;
    margin: 20%;
    border-radius: 10px;
    color: $black;
    #text {
      font-family: "Amiri", serif;
    }
    #author {
      font-family: "Neucha", cursive;
      font-size: 2.5em;
    }
    .btn-row {
      display: flex;
      flex-direction: row;
      justify-content: flex-end;
      #tweet-quote {
        margin-left: 1em;
      }
    }
  }
  @media only screen and (max-width: 480px) {
    #quote-box {
      margin: 0;
      overflow-y: auto;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Using Stylesheets

Angular

In Angular, component-specific stylesheets are their own separate files within a component directory, and imported via the @Component() decorator styleUrls property inside the component's TypeScript (.ts) file. This decorator and its properties will be automatically generated by the Angular CLI when using ng new or ng generate component.

// src/app/app.component.ts (Angular)
// ...

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
  // ...
}
Enter fullscreen mode Exit fullscreen mode
// src/app/quote-box/quote-box.component.ts (Angular)
// ...

@Component({
  selector: "app-quote-box",
  templateUrl: "./quote-box.component.html",
  styleUrls: ["./quote-box.component.scss"],
})
export class QuoteBoxComponent {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The global stylesheet at src/styles.scss in Angular seems to be automatically imported and applied at the app level without any modifications to the app module or component code.

React

In React, component-specific stylesheets can just be imported into the component JavaScript file just like a typical JavaScript import.

// src/App.js (React)
import React from "react";
import QuoteBox from "./components/QuoteBox";
import "./App.scss";

const App = () => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode
// src/components/QuoteBox.js (React)
import "./QuoteBox.scss";

const QuoteBox = (props) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

The global stylesheet at src/index.scss is imported at the top of src/index.js.

// src/index.js (React)
import React from "react";
import ReactDOM from "react-dom";
import "./index.scss"; // <-- import global stylesheet here
import App from "./App";
import reportWebVitals from "./reportWebVitals";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
// ...
Enter fullscreen mode Exit fullscreen mode

Vue

As seen previously, component-specific styles in Vue are placed inside style tags at the bottom of a .vue component file. The contents there aren't imported by the JavaScript in the script tag section, and seem to automatically be applied to the component.

Global stylesheets, on the other hand are imported much like in Angular and React. It will be imported in src/main.js like so:

// src/main.js (Vue)
import Vue from "vue";
import App from "./App.vue";
import "./styles/styles.scss"; // <-- import global stylesheet here

Vue.config.productionTip = false;

new Vue({
  render: (h) => h(App),
}).$mount("#app");
Enter fullscreen mode Exit fullscreen mode

Logic

App logic in Angular is handled in TypeScript, and in the other two with JavaScript, with the option of adding TypeScript if desired. For these I chose the default route of using JavaScript, but it's fairly easy to switch to TypeScript with either React or Vue.

App Logic

Angular

With Angular, the application logic resides in the AppComponent class inside src/app.component.ts.

// src/app/app.component.ts (Angular)
import { Component, OnInit } from "@angular/core";

interface Quote {
  quote: string;
  author: string;
}

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
  loading: boolean = true;
  quote!: Quote;
  quoteList!: Quote[];
  tweetURL!: string;
  getNewQuote: () => void = (): void => {
    const idx = Math.floor(Math.random() * this.quoteList.length);
    const newQuote = this.quoteList[idx];
    this.quote = newQuote;
  };

  constructor() {}

  ngOnInit() {
    this.fetchData();
  }

  async fetchData(): Promise<void> {
    const quotesURL =
      "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
    const response = await fetch(quotesURL);
    const quotes = await response.json();
    const idx = Math.floor(Math.random() * quotes.quotes.length);
    const newQuote = quotes.quotes[idx];
    this.quoteList = quotes.quotes;
    this.quote = newQuote;
    this.setTweetURL(newQuote);
    this.loading = false;
  }

  setTweetURL(quote: Quote): void {
    this.tweetURL = `https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=${quote.quote} --${quote.author}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

React

In React, it's either a function or class inside src/App.js. In this case, it's the App arrow function there.

// src/App.js (React)
import React from "react";
import QuoteBox from "./components/QuoteBox";
import "./App.scss";

const App = () => {
  const [loading, setLoading] = React.useState(true);
  const [quote, setQuote] = React.useState({});
  const [quoteList, setQuoteList] = React.useState([]);
  const [tweetURL, setTweetURL] = React.useState("");

  const getNewQuote = () => {
    const idx = Math.floor(Math.random() * quoteList.length);
    const newQuote = quoteList[idx];
    setQuote(newQuote);
  };

  const fetchData = async () => {
    const quotesURL =
      "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
    const response = await fetch(quotesURL);
    const quotes = await response.json();
    const idx = Math.floor(Math.random() * quotes.quotes.length);
    const newQuote = quotes.quotes[idx];
    setQuoteList(quotes.quotes);
    setQuote(newQuote);
    setTweetURL(
      `https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=${newQuote.quote} --${newQuote.author}`
    );
    setLoading(false);
  };

  React.useEffect(() => {
    fetchData();
  }, []);

  return loading ? (
    <div id="loading">
      <h1>loading...</h1>
    </div>
  ) : (
    <QuoteBox
      quote={quote.quote}
      author={quote.author}
      getNewQuote={getNewQuote}
      tweetURL={tweetURL}
    />
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Vue

In Vue, it's the script tag section of src/App.vue.

<!-- src/App.vue – script section (Vue) -->
<script>
  import QuoteBox from "./components/QuoteBox.vue";
  export default {
    name: "App",
    components: {
      QuoteBox,
    },
    data() {
      return {
        loading: true,
        quote: {},
        quoteList: [],
        tweetURL: "",
      };
    },
    created() {
      this.fetchData();
    },
    methods: {
      async fetchData() {
        const quotesURL =
          "https://gist.githubusercontent.com/camperbot/5a022b72e96c4c9585c32bf6a75f62d9/raw/e3c6895ce42069f0ee7e991229064f167fe8ccdc/quotes.json";
        const response = await fetch(quotesURL);
        const quotes = await response.json();
        const idx = Math.floor(Math.random() * quotes.quotes.length);
        const newQuote = quotes.quotes[idx];
        this.quoteList = quotes.quotes;
        this.quote = newQuote;
        this.tweetURL = `https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=${newQuote.quote} --${newQuote.author}`;
        this.loading = false;
      },
      getNewQuote() {
        const idx = Math.floor(Math.random() * this.quoteList.length);
        const newQuote = this.quoteList[idx];
        this.quote = newQuote;
      },
    },
  };
</script>
Enter fullscreen mode Exit fullscreen mode

QuoteBox Logic

There isn't really much "logic" in the QuoteBox component in any case; it's mostly just a display component defining the UI given some values from the parent app component.

Angular

For Angular, the QuoteBoxComponent class is defined in src/app/quote-box/quote-box.component.ts.

import { Component, Input } from "@angular/core";

@Component({
  selector: "app-quote-box",
  templateUrl: "./quote-box.component.html",
  styleUrls: ["./quote-box.component.scss"],
})
export class QuoteBoxComponent {
  @Input() author!: string;
  @Input() quote!: string;
  @Input() tweetURL!: string;
  @Input() getNewQuote!: () => void;

  constructor() {}
}
Enter fullscreen mode Exit fullscreen mode

Notice the @Input() decorator on each of these class variables. What this essentially means is a parent component will be providing values to these as inputs. Essentially all this class does is receive values from the parent and then inject them into the template due to the corresponding bindings.

React

In the React version of this project, the QuoteBox component logic is defined as a very simple arrow function in src/components/QuoteBox.js.

// src/components/QuoteBox.js (React)
import "./QuoteBox.scss";

const QuoteBox = (props) => {
  return (
    <div id="quote-box">
      <h1 id="text">
        <i className="fa fa-quote-left"></i> {props.quote}
      </h1>
      <p id="author">- {props.author}</p>
      <div className="btn-row">
        <button
          className="btn btn-primary"
          id="new-quote"
          onClick={props.getNewQuote}
        >
          New quote
        </button>
        <a
          id="tweet-quote"
          href={props.tweetURL}
          target="_top"
          className="btn btn-secondary"
        >
          <i className="fa fa-twitter"></i> Tweet
        </a>
      </div>
    </div>
  );
};

export default QuoteBox;
Enter fullscreen mode Exit fullscreen mode

The props parameter is essentially an object where the parent passes data down to a child. In the parent's JSX return statement, these values will appear like attributes assigned to either literal values or expression values. The QuoteBox function's returned JSX looks almost exactly like the layout templates in Angular and Vue. Again the only thing this function really does is serve to inject given prop values into a UI template defined by the JSX.

Unlike in Angular and Vue, where the component name is defined by initializing a string variable, the exported function or class name itself serves as the expected identifier of the component for use with other components' JSX in React.

Vue

In Vue, the QuoteBox is again very similar, and does basically the same exact thing, but with even less code, in the script section of src/components/QuoteBox.vue.

<!-- src/components/QuoteBox.vue – script section (Vue) -->
<script>
  export default {
    name: "QuoteBox",
    props: {
      quote: String,
      author: String,
      tweetURL: String,
      getNewQuote: Function,
    },
  };
</script>
Enter fullscreen mode Exit fullscreen mode

here we define a props object in a more traditional looking way that React does in JSX. The props seem to work a lot like in React. The parent will pass down these values from the template and logic. This component will just receive them as values and sync them with the component template. Again, just receiving the values from the parent app component and placing them in the QuoteBox template.

the name property here works pretty much exactly like the selector property of the @Component() decorator in Angular. It defines the expected name of this component for use in other Vue templates.

Impressions

Overall, I found it fairly easy to get the basics down in all three of Angular, React, and Vue for a small project such as this. The documentation for all three is really good and well maintained. Each has its own strengths and weaknesses, but for a project like this one, I found that personally there is almost no difference in the learning curve and general ease of use. It's difficult for me to choose a favorite here, and I don't think that there are any "winners" or "losers", "better" or "worse". It comes down to what you're used to and what you like. Out of the three, I definitely have had much more exposure to React and like it a lot. But after using Angular and Vue, I really like them too, just about as much. I will be using Angular for work, so I thought it would be good to dive in and convert something familiar in React to Angular. And just for kicks, also to Vue, since I see that is rising rapidly in popularity these days.

My general impressions about the similarities and differences is that they are so similar, that it is in my opinion quite easy to go from using one to the other between the three. I'd almost go so far as to oversimplify and sum it up as "It's just syntax".

For the use cases, I would say that the impression I got from working with Angular is that it seems very much suited for large scale enterprise applications right out of the box. This isn't quite a fair comparison to make because Angular and Vue are fully considered to be frameworks and React is a library. And, obviously, React is used in tons of large scale corporate projects (and I'm sure Vue is as well). React to me feels like a lightweight library for writing UIs quickly and easily, and it mostly stays out of the way, having no real opinions about structure, and allows for a lot of customizations. I've found that Angular and Vue seem to have opinions and more rigid structure, which probably comes from them being frameworks having established conventions more so than React.

Discussion (2)

Collapse
hakimio profile image
Tomas Rimkus • Edited

That's a strange implementation. Your quote-box should be only a "display" component. It doesn't need getNewQuote() function as an input. To implement it properly in Angular, you would create getNewQuoteClick output/event emitter in your "quote-box" component and then when the event is triggered you would set a new quote from your parent component. Also, it might be a good idea to introduce a new service for getting quotes instead of putting all the logic in the app.component.ts. And one more small remark: we use HttpClient service in Angular instead of fetch().

Collapse
corscheid profile image
Corey Scheideman Author

Thanks for the great info and suggestions here! I'll fix these things and update soon. I knew something was a bit off, but now that you bring some of that up, I'm wondering how I didn't think of it. Cheers!

Forem Open with the Forem app