Comparing JavaScript Frameworks part 1: templates

2024-03-11

In this blog post series I will compare the following JavaScript frameworks: Vue.js, React, Angular, and Svelte.

In part 1 the focus will be on comparing JavaScript framework template languages. The template language of a JavaScript framework is used to define the HTML of the application / website.

You can say that the main reason any of the JavaScript frameworks exists is the following: to make creating dynamic pieces of HTML easier. So the template language it is a rather important part of a framework.

The template languages differ greatly. The question is are they different enough?

Comparing JavaScript frameworks part 1: templates. Comparing the template languages of React, Vue, Angular and Svelte.

My qualifications

I'm working on a project called uiloos it is a headless component library written in JavaScript which has bindings to React, Angular, Svelte and Vue, and as such I'm in a position to have experienced them all.

I've first hand seen their strengths and weaknesses and I thought it would be fun and informative to share them.

Out of transparency I'd like to point out that I provide courses in React. So out of all frameworks discussed I feel like I know React best. Second thing is that I've also worked for about a year on a large Angular (2.x) application.

Defining Components

Lets get started!

All frameworks work with a component based model. A component can be seen as a custom HTML tag / element, which builds on top of pre-existing HTML element, or other components.

Lets take a look at how to define components, and focus on the overall structure first: where do you put your HTML (templates), CSS and JavaScript logic?

Lets look at various "Hello world" components, first React:

// HelloWorld.tsx

// React has no CSS story on its own
// but bundlers often provide ways
// to import CSS:
import './HelloWorld.css';

export function HelloWorld() {
  // JS logic goes here, before 
  // the return.
  const message = 'Hello world!';

  // You return the template here:
  return <p>{message}</p>;
}

Lets look at Angular next:

// hello-world-component.ts

// Angular tends to split HTML, CSS and JS 
// into 3 separate files, and but for 
// smaller components an inline template is
// often used.

// @Component is a decorator, which takes a 
// JS class and turns it into an Angular
// component.
@Component({
  selector: 'hello-world',
  standalone: true,
  styleUrl: './hello-world.component.css',
  templateUrl: './hello-world.component.html',
})
export class HelloWorldComponent {
  // The JS logic you put in a class.

  public message: 'Hello World';
}

// File: hello-world.component.html
<p>{{ message }}</p>

Now for the Svelte version:

// HelloWorld.svelte

// The template of the component
<p>{ message }</p>

<script>
  // JS Logic goes inside of a script tag
  const message = 'Hello world!';
</script>

<style>
  // Any CSS goes here in the same file
</style>

and finally the Vue version:

// HelloWorld.vue

<script setup>
  // JS Logic goes inside of a script tag
  const message = 'Hello World!';
</script>

// HTML goes into a template tag
<template>
  <p>{{ message }}</p>
</template>

<style>
  // Any CSS goes here in the same file
</style>

Vue and Svelte both converged on the idea of putting everything in a single file, and using a compiler to sort things out.

I'll admit that this approach of putting everything in a single file was a bit off putting at first. Now that I've used it for some time I find that it actually works quite well.

Angular in turn splits everything up into multiple files, it is also possible to defined the template inline, but for larger components this is not often done. The result is that Angular projects have a lot of files!

Angular feels like it is the most verbose out of all frameworks. There are a lot of concepts flying about compared to the other three. Also Angular makes me feel like I'm writing Java and not JavaScript, due to its heavy use of classes and decorators.

Reacts "template language" JSX allows you to write XML inside of JavaScript through a precompile step. For me React feels the most like I'm writing JavaScript, even with JSX in the mix.

CSS wise, React is the odd man out, it does not have any CSS support out of the box, and requires either a bundler or a CSS-in-JS library.

In contrast: Vue, Svelte and Angular the CSS can be automatically scoped to only your component. This prevents your components CSS from bleeding out to the rest of the website / application.

Interpolation

In the examples above each framework renders a message variable that contains the string "Hello world!". The act of replacing variables in templates with the actual content is called interpolation

For interpolation there are two schools of thought, single or double curly braces:

<!-- Svelte and React -->
<p>Hi my name is { name }</p>

<!-- Angular and Vue -->
<p>Hi my name is {{ name }}</p>

The camps are split evenly: React and Svelte use single curly braces and Angular and Vue use the double curly braces. The differences here are really superficial, you might prefer one of the other, but one is not better than the other.

It gets more interesting when you decide to bind attributes. Take a look at how you bind an alt attribute of an <img> tag to a variable:

<!-- Svelte and React -->
<img alt={ alt } />

<!-- Svelte shorthand -->
<img {alt} />

<!-- Vue -->
<img :alt="alt" /> 

<!-- Vue shorthand -->
<img :alt /> 

<!-- Angular -->
<img alt="{{ alt }}" />

Here you can see that Vue makes an interesting choice: every dynamic attribute is prefixed by : (colon). Without a colon it will be treated as a regular HTML attribute, without any binding.

It gets even more interesting when you decide to write interpolated attributes: (the syntax highlighting has been disabled)

<!-- React -->
<img alt={`Picture of ${name}`} />

<!-- Svelte -->
<img alt="Picture of {name}" />

<!-- Vue -->
<img :alt="`Picture of ${name}`" />

<!-- Angular -->
<img alt="Picture of {{ name }}" />

Angular and Svelte here feels like they has the least amount of noise, in my opinion they look the most elegant. This is because they allow for interpolation inside of attribute strings.

Vue feels like the most heavy of the three, due to the colon, and quotes and backticks.

In React and Svelte each time you open a curly brace pair lets you can write a JavaScript expression. So inside React we must create a template literal expression (backticks) so we can write a dynamic string. Which looks quite ugly in my opinion.

One of the features I'd like to see in React and Vue is that they allow this:

<img alt=`Picture of ${name}` />

Or simply interpolate inside of strings like Angular and Svelte do. It would make the templates in React and Vue so much less noisy.

Now for the final boss of interpolation lets create a dynamic aria-label attribute:

// Svelte and React
<button aria-label={`Close modal ${name}`>X</button>
<!-- Vue -->
<button :aria-label="`Close modal ${name}`">X</button>

<!-- Angular -->
<button [attr.aria-label]="getLabel()">X</button>

Here Svelte, React and Vue do not surprise us, there is no difference between setting an alt attribute or the aria-label attribute.

But what is happening in Angular? The thing about Angular is that it always tries to do the correct thing, even though it results in ugly code. This will be a recurring theme throughout this blog post.

Technically speaking there are differences DOM / JS properties and HTML attributes. HTML attributes initialize DOM properties, and so Angular reasons:

"In Angular, the only role of HTML attributes is to initialize element and directive state."

In other words you should use the special [], in Angular terms attribute binding, syntax for properties. This distinction feels a bit silly, and this always makes me doubt if I'm using the correct syntax!

Another thing to note is that we must now call a function in our case getLabel() since you cannot use interpolation when using an attribute binding.

I understand that what Angular is trying to do is the "right thing" but it just feels so wrong to me!

Properties / attributes

Regular plain old HTML elements can have attributes, for example the src attribute on an <img> tag. JavaScript frameworks also support creating attributes on your own components.

Lets create a simple component called Greeter: which takes a name attribute and simply greets the provided name.

Lets start with Svelte:

<script>
  /* 
    In Svelte all exported bindings are 
    considered "props" or "properties" 
    of the component, and can be used 
    in the template.

    This works because Svelte's compiler
    treats "export" as having a different 
    meaning than regular JS.
  */
  export let name = "";
</script>

<p>Hello {name}</p>

Next up is Vue:

<script setup lang="ts">
  /* 
    In Vue you define props using the 
    compiler macro called "defineProps"
    which can take a TypeScript type
    definition.

    A macro is a piece of code that 
    gets substituted with expanded
    code at compile time.
  */
  defineProps<{
    name: string;
  }>();
</script>

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

Now for Angular:

import { Input } from '@angular/core';
import { Component } from '@angular/core';

@Component({
  selector: 'app-greeter',
  standalone: true,
  template: `
    <p>Hello {{name}}</p>
  `,
})
export class GreeterComponent {
  @Input() name!: string;
}

And finally React:

export function Greeter(props) {
  /* 
    In React all attributes are provided
    as an object which React convention 
    calls "props".

    When the JSX is "transpiled" to regular 
    JS function calls all props are collected
    and passed to the component function as 
    the first argument.
  */
  return <p>Hello {props.name}</p>;
}

In Vue and Svelte use the idea of a compiler extensively, but there is a major difference: whereas Vue creates new constructs in the defineProps macro, Svelte opts to change what existing JavaScript syntax means.

To me Svelte's hijacking existing JavaScript syntax like this feels wrong. It is kind of a bit unexpected, and it forces me to always make a mental note that I'm looking at Svelte's JavaScript dialect and not pure JavaScript.

I'm not the only one with this complaint! As a response to this flaw the creators of Svelte are going to introduce a new API. It is called: "Runes" and in this post called "Introducing runes" you can read all about it.

If you know React: runes will look a lot like React hooks. I you know Vue: runes will look a lot like Vue composables.

Angular is the only one of the four that does not use the term props, and instead calls it Input. I think this is simply because React popularized the term and the rest followed.

Using Components

We have seen how to define components lets use them now:

<!-- Svelte, React, and Vue -->
<Greeter name="Maarten" />

<!-- Angular -->
<app-greeter name="Maarten" />

Components show up in the HTML in all frameworks as regular HTML elements, but most frameworks start their components with capital letters, only Angular is an outlier here.

Angular does this in order to stay close to the custom elements aka web components spec.

The other frameworks choose / prefer a capital letter so they are instantly recognizable as being React / Vue / Svelte component.

There is also another reason for the capital letter: custom elements / web components must always start with a lower cased letter according to the spec. So by preferring a capitalized name collisions are avoided.

Vue actually allows both to be used at the same time but prefers that you use PascalCase for the reasons listed above.

Lets change the scenario slightly: what if the name was not hardcoded but a variable instead, and we want the greeting to change whenever the variable changes:

<!-- Svelte and React -->
<Greeter name={name} />

<!-- Svelte shorthand -->
<Greeter {name} />

<!-- Vue -->
<Greeter :name="name" />

<!-- Vue shorthand -->
<Greeter :name/>

<!-- Angular -->
<app-greeter [name]="name" />

Here we see an interesting split Svelte and React do the same thing, but Vue goes in another direction.

In Vue all bindings of JavaScript variables must be prefixed with an : to denote an attribute binding.

Angular lets you create bindings by surrounding the name of the attribute with [] square brackets.

What you start to see here is an interesting split: Angular and Vue tend to want to expand HTML further. With their template syntax staying closer to HTML.

This is apparent in the choice to include the " quotes around attributes in all documentation. Even though in both Vue and Angular the quotes (") are actually optional.

Conditional rendering

Every template language needs a way to render different HTML based on certain conditions.

In the following examples we either render a "Log in" or "Log out" button based on whether or not a user object is defined.

Lets look at two Angular examples first, the old and the new syntax (2024):

<button *ngIf="user">Log out</button>
<button *ngIf="!user">Log in</button>
@if (user) {
  <button>Log out</button>
} @else {
  <button>Log in</button>
}

Next up Vue:

<button v-if="user">Log out</button>
<button v-else>Log in</button>

Now for React:

{user ? (
  <button>Log out</button>
) : (
  <button>Log in</button>
)}

Last but not least Svelte:

{#if user}
  <button>Log out</button>
{:else}
  <button>Log in</button>
{/if}

Lets start with an observation about React: React does not really have a template language. Instead it allows you to write JavaScript expressions in between two {} curly braces.

The downside to Reacts approach is that only expressions are allowed and no statements. Meaning no if-statements, or for-loops, etc etc. This is why the ternary expression must be used because it is an expression.

The upside to Reacts is that you do not need to learn a new template language, and instead rely on JavaScript itself. Which is great if you know JavaScript but perhaps a bit daunting if you do not.

Svelte and React have been in lockstep in both "Interpolation" and "Props" but here Svelte and React part ways. Svelte opts for using a template language reminiscent of Twig (PHP), ERB (Ruby), and others.

One thing to note about Svelte here is that I'm always tripping up over the colon in the :else. I keep forgetting it, or I prefix it with an # instead. In this regard I find Angulars new syntax a little more consistent.

What I find most interesting here though, is the comparison between the old Angular syntax and Vue. Vue and Angular have these things called directives:a directive looks like an HTML attribute, but with special meaning within the template syntax. In our examples *ngFor and v-if, v-else are directives that conditionally render things.

The benefits of directives is that it makes everything look like HTML, but the other benefit is that you can send it down the wire! Vue is particularly known for easy integration with server side languages. The PHP framework Laravel has embraced Vue for this exact reason!

But why then is Angular abandoning directives? The reason I think is that the syntax can also be a bit noisy. It is not always easy to identify parts of the template syntax, or quickly see relationships such as with v-if or v-else.

Also directives do not lend themselves to indentation very much. Indentation in if-statements makes them easier to read, and to determine what the branches actually are.

If I had to choose between Angular directives or Vue directives, I would choose Vue. Angular's insistence of prefixing its directives with *ng makes it look uglier than Vue's v- prefix in my opinion.

So current Angular looks like Vue but modern Angular will look more like Svelte. These recent changes in Angular are referred by the community as the "Angular renaissance".

Now I want to take a look at how to write multiple conditions, in the example we want to show different things based on the age:

Again lets look at the React first:

{age === 0 ? (
  <p>You are a baby</p>
) : age < 18 ? (
  <p>You are a child</p>
) : (
  <p>You are an adult</p>
)}

Next up Svelte:

{#if age === 0}
  <p>You are a baby</p>
{:else if age < 18}
  <p>You are a child</p>
{:else}
  <p>You are an adult</p>
{/if}

And now the two Angular versions:

@if (age === 0) {
  <p>You are a baby</p>  
} @else if (age < 18) {
  <p>You are a child</p>
} @else {
  <p>You are an adult</p>
}
<p *ngIf="age === 0">You are a baby</p>  
<p *ngIf="age > 0 && age < 18">You are a child</p>
<p *ngIf="age >= 18">You are an adult</p>

Lets close of with Vue:

<p v-if="age === 0">You are a baby</p>  
<p v-else-if="age < 18">You are a child</p>
<p v-else>You are an adult</p>

Here React sticks out like a sore thumb, as the nested ternary expressions look very foreign. I'm pretty used to looking at nested ternaries as an experienced React developer, but as someone teaching React courses this is where a lot of students start scratching their heads.

In React when there are multiple ternary expressions the advice is to refactor them out as separate functions or components.

In contrast I find the "new" Angular and Svelte versions very aesthetically pleasing to look at. This is due to the natural indentation / nesting the blocks have.

The "old" Angular versions is also unique: it requires age > 0 && age < 18 for the "you are a child" path. This is because I wanted to avoid using an else. Old Angular does support an else, but it is so vile, it has to be seen in order for you to believe it:

<!-- Taken verbatim from the angular.io docs -->
<div *ngIf="show; else elseBlock">Text to show</div>
<ng-template #elseBlock>
  Alternate text while primary text is hidden
</ng-template>

It is no wonder the Angular team wanted to improve the template language! The old way of doing if-else statements in Angular is utterly confusing.

The Vue variant improves on Angular by having an v-else-if, but still the lack of natural indentation makes less visible.

Lists and loops

In our applications / pages it is very common to loop over collections / arrays and to show them. For example looping over a list of persons and rendering their names.

Take a look at React first:

<ul>
  {persons.map(person => 
    <li key={person.name}>{person.name}</li>
  )}
<ul>

This time Vue is second:

<ul>
  <li 
    v-for="person in persons" :key="person.name"
  >
    {{person.name}}
  </li>
</ul>

And Svelte is third:

<ul>
  {#each persons as person (person.name)}
    <li>{person.name}</li>
  {/each}
</ul>

We will do Angular last, both old and new syntax:

<ul>
  <li 
    *ngFor="let person of persons trackBy: personsTrackBy"
  >
    {{person.name}}
  </li>
</ul>
<ul>
  @for (person of persons; track person.name) {
    <li>{{person.name}}</li>
  }
</ul>

First lets explain the key / trackBy you see in the examples: these allow the frameworks to keep better track of the items in the list. They make the code more performant because the frameworks can now, with laser precision, update the DOM when the order of the list changes or when an item of the list itself changes.

In React I've always been annoyed at the noise a map makes. It has a => and boatloads of curly braces. This makes them somewhat hard to read and write.

Angular's *ngFor approach is an absolute train wreck, with the weird let and always bizarre *ng. Stranger still is the trackBy which must point to a method in the component, and cannot be inlined. The win gained by the new @for syntax is huge!

The second Angular version using @for is great. The only critique I have is the use of of instead of in, which I feel is more natural. The reason for using of is because JavaScript uses of as well.

Now for Svelte: I find Svelte's choice here does not jive with me for these two reasons:

  • for instead of each would have been more clear to me. Perhaps the hypothetical for could then use a person in persons like in Vue.
  • I do not like the way that the key is denoted between the (), I like Angular's track better.

Finally Vue with the v-for I find quite nice, I also like the way the :key is an attribute on the child. This way no special syntax inside of the v-for is needed.

Events handling

Next I want to take a look at how the various template languages in the frameworks handle dealing with events, such as click, mouse over and drag and drop.

Lets focus on the on click event, as I think it is most common. Also Ignore reactivity for now as I want to focus on this in the second part of this series.

<!-- Vue -->
<button @click="increment($event)">{{ count }}</button>
<button @click="count += 1">{{ count }}</button>

<!-- Angular -->
<button (click)="increment($event)">{{ count }}</button>

<!-- React -->
<button onClick={ increment }>{count}</button>
<button onClick={ (event) => setCount(count + 1) }>{ count }</button>

<!-- Svelte -->
<button on:click={ increment }>{count}</button>
<button on:click={ (event) => count += 1 }>{ count }</button>

As you can see all four frameworks differ greatly from each other in the way events work from inside of the templates. I honestly do not have any favorites here.

Vue is special: it allows mutation directly from inside of the template. Angular only supports mutation via =, but not +=, or -= etc etc.

In both Vue and Angular the way to get access to the event is to use the $event special template variable.

In Svelte and React you must provide a function in order for events to work. These functions are then called with the first parameter being the event.

React also sets itself apart: it is the only framework where you do not get the actual event that occurred, instead you get a so called "Synthetic Event". According to React this is to fix browser inconsistencies, but I suspect this is not quite as necessary as it once was. I hope this wart on the React API can be removed in the future.

Another aspect of events is handling modifier keys and preventing default browser behavior:

<!-- Vue -->
<button @click.shift.alt="increment($event)">{{ count}}</button>
<button @click.prevent="increment($event)">{{ count}}</button>
<button @click.ctrl.prevent="increment($event)">{{ count}}</button>
<button @click.ctrl.prevent.once="increment($event)">{{ count}}</button>

<!-- Angular -->
<button (click.shift.alt)="increment($event)">{{count}}</button>>

<!-- Svelte -->
<button on:click={increment}>{count}</button>
<button on:click|preventDefault={increment>{count}</button>
<button on:click|preventDefault|once={increment>{count}</button>

Out of all frameworks Vue provides the most event modifiers so you never have leave the template language.

Angular only provides the key modifiers, whereas Svelte only gives you event modifiers. Which is interesting as you might expect them to just give you both.

React is missing because it does not provide these "shortcuts", instead you must handle these use cases in JavaScript / TypeScript yourself.

I think the idea of React here is that JavaScript already supports all of this why add them to JSX. For the same reason no "looping" syntax exists.

I'm torn about this, on the one hand in Vue you get all of these goodies. At the cost of a more complicated template language. React on the other hand keeps things simpler, but at the cost of writing more JavaScript.

Class and style

Another thing I want to compare is how to the frameworks deal with setting conditional CSS classes and style attributes.

// React
<p className={ age < 18 ? 'red' : 'green'}></p>
<p className={classNames({ red: age < 18, green: age >= 18 })}></p>

In React there are two things to notice, first is that the attribute is called className instead ofclass. The reason for this is because JSX sides with JavaScript and not HTML. I've personally have always been annoyed that JSX does this, it makes copying HTML a chore.

Second thing about React, is that it does not support dynamic CSS classes out of the box. In the example above the classNames comes from the classnames library. Again this is a common refrain in React, it only wants to do the bare minimum.

The benefit is that React gives you a choice, the downside is that you now have a choice. Do I use clsx or classnames? Do I even care? Do I want to care?

Next up is Angular:

<!-- Angular -->
<p [ngClass]="age < 18 ? 'red' : 'green'"></p>
<p [ngClass]="{ red: age < 18, green: age >= 18 }"></p>
<p [class.red]="age < 18" [class.green]="age >= 18"></p>

I love the fact that Angular gives you three ways of setting CSS classes. Each with their own strengths and weaknesses. The last variant is incredibly readable for when you have multiple CSS classes that do not exclude each other.

Svelte you are up:

// Svelte
<p class={ age < 18 ? 'red' : 'green'}></p>
<p class:red={age < 18} class:green={age >= 18}></p>

Taking a page from Angular's playbook Svelte also supports the "can set classes separately" power move.

Svelte does not support sending an object, but I think this is a deliberate choice a I find that sending an object creates noise.

Finally for Vue:

<!-- Vue -->
<p :class="age < 18 ? 'red' : 'green'"></p>
<p :class="{red: age < 18, green: age >= 18}"></p>

Given Vue's extremely feature rich template syntax I was surprised that it does not support setting CSS classes separately like Angular and Svelte!

In this aspect I think Angular is a winner here!

As for style lets take a look at Vue first, note that in all examples color is a variable:

<!-- Vue -->
<p :style="{backgroundColor: color}"></p>
<p :style="{'background-color': color}">

Vue only supports giving style an object. In that object you can either use the CSS or JS name for the property you want to style.

Angular is up next:

<!-- Angular -->

<p [ngStyle]="{ backgroundColor: color}"></p>

<p [style.backgroundColor]="color"></p>

<p [style.background-color]="color">

<!-- Works but not promoted / documented -->
<p style="background-color: {{color}}">

Angular support multiple ways of setting the style, including setting styles one property at a time which I like.

The last variant is not documented anywhere, and I suspect this is because it relies on string interpolation, which is flakey for this purpose.

Svelte's turn:

// Svelte
<p style:background-color={color}>
  Your age is
</p>

<p style:background-color|important={color}>
  Your age is
</p>

// Works but not promoted / documented
<p style="background-color: {color}">
  Your age is
</p>

Svelte again does not support setting style via an object, like it does for classes, but I prefer setting it separately anyway. A nice feature is that you can also make a style CSS important.

Svelte like Angular also allows string interpolation, but it is not documented / promoted.

Last and least React:

// React
<p style={{ backgroundColor: color}}></p>

React only supports one way of setting styles via an object, which follows the JavaScrip convention with the keys.

Again I'm really missing the ability to set styles separately. It is such a nice feature to have!

Slotting

The last aspect I want to take a look at is the ability to slot child content into a custom component.

Lets start with a simple example in which there is only one slotted element, a simple "card" component:

Lets begin with React:

// React

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

// Usage

<Card>
  <p>This is the content</p>
</Card>

No it is Vue's turn:

<!-- Card.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>

<!-- Usage -->

<Card>
  <p>This is the content</p>
</Card>

The Svelte version looks very much like the Vue version:

<!-- Card.svelte -->
<div class="card">
  <slot />
</div>

<!-- Usage -->

<Card>
  <p>This is the content</p>  
</Card>

The Angular version is up next:

// card.component.ts
import { Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-card',
  template: `
    <div class="card">
      <ng-content></ng-content>
    </div>
  `,
})
export class CardComponent {}

// Usage

<app-card>
  <p>This is the content</p>
</app-card>

Svelte and Vue look so similar, the only difference is the <template> element. I like that there is also no JavaScript needed at all to make this work. Also nice the use of HTML <slot> element, which was created for this purpose, it stays very close to the spirit of HTML.

In React the content between a custom component is placed in a special prop called: children. Which you can then render to project the content in a specific place.

In Angular you use a custom element called <ng-content> which in hindsight should have perhaps been <slot> as well.

I want to touch on something here: sometimes a things / terminology exist in HTML but the frameworks use something different. Such as in this case slotvs children / ng-content. I think one explanation for this is that sometimes the frameworks named it before the equivalent in HTML existed.

I guess for creators of frameworks you then have a choice: keep using the old name and keeps things stable. Or rename them and create legacy code, write migration docs, support both syntaxes etc etc. It must be a difficult choice.

Al in all the four frameworks can achieve this task with minimal lines of code. I do not think one approach here is definitively better than the others.

Next I want to demonstrate how to create a "card" component that has a slot for the content, header and footer. The content is required but the header and footer are optional.

Lets start with React again:

// React

function Card({ header, footer, children }) {
  return (
    <div className="card">
      { 
        header 
          ? <div class="card-header">{header}</div> 
          : null
      }

      <div className="card-content">{children}</div>

      { 
        footer 
          ? <div class="card-footer">{footer}</div> 
          : null
      }
    </div>
  );
}

// Usage

<Card 
  header={<h1>This is the header</h1>}
  footer={<em>This is the footer</em>}
>
  <p>This is the content</p>
</Card>

In React all props (attributes) can be JSX, so in the React model you simply add more named props and render them.

To make them conditional you wrap them around a ternary expressions, and render null to make React render nothing if they are not provided.

In React using "children" feels natural, but other render props do not. When using the Card the header and footer are defined before the content. This runs contrary to a more natural order: header, content and then footer.

As you will see below only React suffers from this problem!

There are two ways around this in React: one is providing children as a props instead, this way you can determine the order yourself. The second is renaming children to say content, and use content as a prop.

Next up Vue:

// Card.vue
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

// Usage

<Card>
  <template #header>
    <h1>This is the header</h1>
  </template>

  <template #default>
    <p>This is the content</p>
  </template>

  <template #footer>
    <em>This is the footer</em>
  </template>
</Card>

When rendering multiple slots you now have to use a HTML <template> tag, and give them a so called ref via the # symbol to name them.

This way Vue knows where to project the <template> inside of the component. The order of the <template> therefore does not matter, you can use any order you want!

Rendering a slot conditionally takes a bit of effort. You have to use the useSlots composable. useSlots gives you reference to all slots as an object. With v-if you then check if the slot is filled or empty.

Update: you can use the $slots variable to get access to all slots. In combination with v-if you then check if the slot is filled or empty.

Next up Svelte:

<!-- Card.svelte -->
<div class="card">
  {#if $$slots.header}
    <div class="card-header">
      <slot name="header" />
   </div>
  {/if}

  <div class="card-content">
    <slot />
  </div>

  {#if $$slots.footer}
    <div class="card-footer">
      <slot name="footer" />
    </div>
  {/if}
</div>

// Usage

<Card>
  <h1 slot="header">This is the header</h1>

  <em slot="footer">This is the footer</em>

  <p slot="default">This is the content</p>  
</Card>

Before Svelte and Vue where twins separated at birth for the single slot use case, now they have grown apart: Svelte uses HTML's own slot attribute instead of the more custom # symbol.

The $$slot is a bit magical, but welcome, as it makes the optional slotting quite easy without any need for JavaScript.

Lets discuss Angular next, and watch as it completely goes of the rails:

I'm showing the complete code needed here as I found the Angular documentation here lacking, I hope someone finds the complete code useful.

// card.component.ts
import { Component, ContentChild } from '@angular/core';
import { CommonModule } from '@angular/common';

import { CardHeaderDirective } from './card-header.directive';
import { CardContentDirective } from './card-content.directive';
import { CardFooterDirective } from './card-footer.directive';

@Component({
  standalone: true,
  selector: 'app-card',
  imports: [
    CommonModule,
    CardHeaderDirective,
    CardContentDirective,
    CardFooterDirective,
  ],
  template: `
    <div class="card">
      <div *ngIf="header?.templateRef" class="card-header">
        <ng-container [ngTemplateOutlet]="header.templateRef"></ng-container>
      </div>

      <div *ngIf="content?.templateRef" class="card-content">
        <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
      </div>

      <div *ngIf="footer?.templateRef" class="card-footer">
        <ng-container [ngTemplateOutlet]="footer.templateRef"></ng-container>
      </div>
    </div>
  `,
})
export class CardComponent {
  @ContentChild(CardHeaderDirective) header!: CardHeaderDirective;

  @ContentChild(CardContentDirective) content!: CardContentDirective;

  @ContentChild(CardFooterDirective) footer!: CardFooterDirective;
}

// card-header.directive.ts
import { Directive, TemplateRef } from "@angular/core";

@Directive({
  standalone: true,
  selector: '[appCardHeader]'
})
export class CardHeaderDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

// card-content.directive.ts
import { Directive, TemplateRef } from '@angular/core';

@Directive({
  standalone: true,
  selector: '[appCardContent]',
})
export class CardContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

// card-footer.directive.ts
import { Directive, TemplateRef } from '@angular/core';

@Directive({
  standalone: true,
  selector: '[appCardFooter]',
})
export class CardFooterDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}

// Usage in main.ts:

import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';

import { CardComponent } from './card.component';
import { CardHeaderDirective } from './card-header.directive';
import { CardContentDirective } from './card-content.directive';
import { CardFooterDirective } from './card-footer.directive';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CardComponent,
    CardHeaderDirective,
    CardContentDirective,
    CardFooterDirective,
  ],
  template: `
    <app-card>
      <ng-template appCardHeader>
        <h1>Hello</h1>
      </ng-template>

      <ng-template appCardContent>
        <p>This is the content</p>
      </ng-template>

      <ng-template appCardFooter>
        <em>This is the footer</em>
      </ng-template>
    </app-card>
  `,
})
export class App {}

bootstrapApplication(App);

If you only looked at usage the code is reasonable, it looks a lot like the Vue approach.

But the amount of code that needs to be written just to get this to work is staggering. Plus the terminology you need to understand: ContentChild, TemplateRef,Directive, ngTemplateOutlet,ngContainer...

It is just to much work! Angular renaissance do your magic!

Conclusion

The differences between the 4 frameworks based on the template languages are very superficial. They all support all use cases, so there is not something you can build only with Vue but not in React etc etc.

Here are some musings about the frameworks in no particular order:

On Angular: if the Angular renaissance had not happened Angular would be dead last to me. I've worked at a company that used to work with Angular 1.x and we saw Angular 2.x and we ran for the hills.

So consider me pleasantly surprised at all the improvements: no more *ngFor, no more custom module system via a standalone components etc etc.

I still feel that Angular is a little to heavy, there is so much javaesque terminology. Also the use of classes feels very heavy compared to React function and Svelte and Vue special file / compiler approach.

I have read that the Angular team wants to tackle the component authoring experience, and I'm curious as to what they come up with!

On React: as a long time React developer I'm looking at some of the other frameworks with a bit of envy. I really like the way Svelte and Angular let you set CSS classes conditionally. I also miss a way quickly handle shift, alt and prevent default on events. And for god sake let me write class instead of className.

Still there is a lot to like: React insistence to not really have much of a template language means I can use my existing JavaScript skills. Also the way props are just arguments to a function has always felt right to me.

So my framework of choice is still React this is due to inertia, I just have the most experience with it, and I know it well. If I started learning front-end now: it would be between Svelte and React. When "Runes" land in the future... oh boy!

On Svelte: has a nice template language, and I'm growing really fond of having a single file that includes JS, CSS and HTML. Also the way the styles are compiled is great, plus the tooling that tells you a CSS selector is not used.

I do find some of Svelte hijacking of existing JavaScript syntax questionable: export meaning something very different for example. For me using Svelte feels like I'm writing in another language that targets the browser. Meaning I always have to thread lightly whenever I write JavaScript in Svelte.

Luckily Runes is set to fix this! Now if they could only make the each work more like my brain works...

On Vue: Vue is a bit strange... it has, objectively, a very good template language! I would not mind to work on a project that uses Vue at all! I just do not like directives very much. For some reason my brain reads over them to easily.

Also the mental model of directives confuses me. I do not find it apparent what happens when you combine multiple directives on one element: what happens when you run v-if in combination with v-for for example. Note that the precedence even changed from v2 to v3!

All and all directives are not my cup of tea, but I know plenty of people that absolutely love them as well. Plus Vue is undeniably easy to integrate with an existing back-end!

I've also said the same about Svelte, but I'm really digging combining HTML, CSS and JS in one file and having a compiler sort it out.

If you take away one thing from this blog post let it be this: in the template aspect it all comes down to your personal tastes.

Stay tuned for the next post in this series

If you want to see more code: this page contains links to examples that implement a calendar component using uiloos. These examples written in React, Vue, Angular and Svelte is what gave me the inspiration to write this post.