Single page applications Flutter, React, Angular, Vue, Web Assembly with C# + Blazor, + Yew, + Seed, Go with Vugu and Web Components with Kotlin/JS and JavaScript

has a few spa frameworks. I’m testing the two most popular ones.

Index

Create a project

Flutter (Dart)

# https://flutter.dev/docs/get-started/web flutter create spa_sample_project cd spa_sample_project flutter run -d chrome --web-renderer html # uses port 51068 by default # canvaskit or auto can also be used as --web-renderer option

React (TypeScript) - create-react-app - terminal (sh like)

# https://create-react-app.dev/docs/adding-typescript # this template adds TypeScript, router and Redux npx create-react-app spa-sample-project --template typescript cd spa-sample-project npm install npm start # uses port 3000 by default

React (JavaScript) - create-react-app - terminal (sh like)

# https://create-react-app.dev/docs/adding-typescript # this template adds JavaScript, router and Redux npx create-react-app spa-sample-project --template redux cd spa-sample-project npm install npm start # uses port 3000 by default

Angular (TypeScript) CLI - terminal (sh like)

# https://cli.angular.io npm install -g @angular/cli ng new spa-sample-project # will always use TypeScript and will ask you about router # PWA support and NgRx (Redux like) must be added later cd spa-sample-project npm install npm start # uses port 4200 by default

Vue (TypeScript) CLI - terminal (sh like)

# https://cli.vuejs.org npm install -g @vue/cli @vue/cli-service-global # the command below should work for windows vue create spa-sample-project # but in my mac I had to use env CC=clang CXX=clang++ vue create spa-sample-project # then manually select features for adding TypeScript, router and Vuex (Redux like) cd create spa-sample-project npm install npm run serve # uses port 8080 by default

Vue (JavaScript) CLI - terminal (sh like)

# https://cli.vuejs.org npm install -g @vue/cli @vue/cli-service-global # the command below should work for windows vue create spa-sample-project # but in my mac I had to use env CC=clang CXX=clang++ vue create spa-sample-project # then manually select features for adding TypeScript, router and Vuex (Redux like) cd create spa-sample-project npm install npm run serve # uses port 8080 by default

Blazor (C#) - terminal (sh like)

# https://docs.microsoft.com/en-us/aspnet/core/blazor/tooling?view=aspnetcore-5.0 # usually project creation through GUI, but the command line is dotnet new blazorwasm -o SpaSampleProject # notice it is blazorWASM, NOT blazorSERVER # no real Redux like support for now cd SpaSampleProject dotnet restore dotnet watch run # uses port 5001 by default

Yew (Rust) - terminal (sh like)

# https://yew.rs/en cargo install wasm-pack cargo install cargo-make # optional cargo install simple-http-server cargo new --lib spa_sample_project cd spa_sample_project_yew

Cargo.toml

[lib] crate-type = ["cdylib", "rlib"] [dependencies] yew = "0.17" wasm-bindgen = "0.2"

Makefile.toml

[tasks.build] command = "wasm-pack" args = ["build", "--dev", "--target", "web", "--out-name", "wasm", "--out-dir", "./static"] watch = { ignore_pattern = "static/*" } [tasks.serve] command = "simple-http-server" args = ["-i", "./static/", "-p", "3000", "--nocache", "--try-file", "./static/index.html"]

terminal (sh like)

cargo make build

Seed (Rust) - terminal (sh like)

# https://seed-rs.org cargo install wasm-pack cargo install cargo-make # optional cargo install simple-http-server cargo new --lib spa_sample_project cd spa_sample_project_seed

Cargo.toml

[lib] crate-type = ["cdylib", "rlib"] [dependencies] seed = "0.8.0" wasm-bindgen = "0.2"

Makefile.toml

[tasks.build] command = "wasm-pack" args = ["build", "--dev", "--target", "web", "--out-name", "wasm", "--out-dir", "./static"] watch = { ignore_pattern = "static/*" } [tasks.serve] command = "simple-http-server" args = ["-i", "./static/", "-p", "3000", "--nocache", "--try-file", "./static/index.html"]

terminal (sh like)

cargo make build

Web Components

React, Angular, Vue and Blazor create git local repositories as part of their starting templates without asking you.
At the time of this writing, I'm using Node.js 15 (15.9.0 specifically) which is not LTS.
The redux-typescript react template failed to install due to the TypeScript version being incompatible and I had to fallback to the original typescript template and manually configure Redux. For this Node.js version, Angular CLI complained about npm 7, and asked me to downgrade to npm 6.
Vue 3 is still in beta.
Vugu is still experimental
And obviously Rust Yew and Seed are still a long way to go when it comes to project templates.
KotlinJS and raw web components don’t really have a starting template. They are not frameworks or libraries per se, but make use of browser APIs.

The size of these projects is what always turned me down. They’re massive with a huge amount of files.

source + libraries compiled initial page load
Flutter 2 (Dart)
React 17.0.1 (TypeScript) 219.1MB+ 1.65MB+
React 17.0.1 (JavaScript) 237.9MB 1.94MB
Angular 11.2.1 (TypeScript) 448.2MB 2.88MB
Vue 3.0.0 (TypeScript) 493.2MB 2.80MB
Vue 3.0.0 (JavaScript) 120.1MB 2.33MB
Blazor (C#) 48.6MB 0.52MB
Yew (Rust) 633.2MB 0.22MB
Seed (Rust)
Vugu (Go)
Kotlin/JS
Web components

Install remaining dependencies

Flutter (Dart)

React (TypeScript)

# https://reactrouter.com/web/guides/quick-start npm install react-router-dom

React (JavaScript)

# https://reactrouter.com/web/guides/quick-start npm install react-router-dom

Angular (TypeScript) CLI - terminal (sh like)

# https://angular.io/cli/add # https://angular.io/guide/service-worker-getting-started ng add @angular/pwa # https://ngrx.io/guide/store/install ng add @ngrx/store@latest --minimal false

Vue (TypeScript)

-

Vue (JavaScript)

-

Blazor (C#)

-

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

Redux + TypeScript template for React will probably get fixed eventually.
Nevertheless, it is interesting to know that Redux is not providing TypeScript support at the same level as JavaScript.
Not sure why Angular doesn’t provide PWA support out of the box.

Create a module and route

Flutter (Dart) - main.dart

import 'package:flutter/material.dart'; // lazy loading using the import deferred as keywords import 'Languages.dart' deferred as LanguagesLoader; void main() { runApp(App()); } class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'SpaSampleFlutter', theme: ThemeData( primarySwatch: Colors.blue, ), routes: <String, WidgetBuilder>{ home: Text('Home'), /* Here we map the path to a component which we will create later FutureBuilder is used to display lazy loaded content Also notice how simple it is to provide alternate content while a module is loading */ '/languages': (BuildContext context) => FutureBuilder( future: LanguagesLoader.loadLibrary(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return LanguagesLoader.Languages(); } else { return Text('Loading...'); } }, ) } /* onGenerateRoute: (settings) { // Handle '/' if (settings.name == '/') { return MaterialPageRoute(builder: (context) => HomeScreen()); } // Handle '/details/:id' var uri = Uri.parse(settings.name); if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'details') { var id = uri.pathSegments[1]; return MaterialPageRoute(builder: (context) => DetailScreen(id: id)); } return MaterialPageRoute(builder: (context) => UnknownScreen()); } */ ); } }

React (TypeScript)

React (JavaScript)

# https://reactjs.org/docs/code-splitting.html cd src/features mkdir languages touch ./languages/Languages.jsx

index.jsx

import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import store from './app/store'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Route } from 'react-router-dom'; ReactDOM.render( <React.StrictMode> <Provider store={store}> <Router> <Route path='/'> <App /> </Route> </Router> </Provider> </React.StrictMode>, document.getElementById('root') );

App.jsx

// lazy loading using the lazy(import()) async functions import React, { lazy, Suspense } from 'react'; import { Switch, Route, Link } from 'react-router-dom'; const Languages = lazy(() => import('./features/languages/Languages')); function App() { return ( <> {/* routes are declarative also notice how simple it is to provide alternate content while a module is loading */} <Suspense fallback={<div>Loading...</div>}> <Switch> <Route path={'/languages'}> {/* here we add a component for this route, which we will create later */} <Languages /> </Route> </Switch> </Suspense> </> ) } export default App;

Angular (TypeScript) CLI

# https://angular.io/guide/lazy-loading-ngmodules # this will create a languages.module.ts where we can declare our components # and add an entry for the module in the routes table ng generate module languages --route languages --module app.module

app-routing.module.ts

// this will be added automatically const routes:Routes = [{ // here we map the path to a module path: 'languages', // lazy loading using the import().then() async function loadChildren: () => import('./languages/languages.module') .then(m => m.LanguagesModule) }];

languages-routing.module.ts

// this will be created automatically import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LanguagesComponent } from './languages.component'; const routes: Routes = [ // and here we map the path to a component which we will create later { path: '', component: LanguagesComponent } ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class LanguagesRoutingModule { }

Vue (TypeScript)

Vue (JavaScript)

# https://router.vuejs.org/guide/advanced/lazy-loading.html mkdir ./src/languages mkdir ./src/languages/components touch ./src/languages/components/Languages.vue

router/index.js

import { createRouter, createWebHistory } from 'vue-router' const routes = [ { // here we map the path to a component which we will create later path: '/languages', name: 'Languages', // lazy loading using the import() async function component: () => import( /* webpackChunkName: "languages" */ '../languages/components/Languages.vue' ) } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router

Blazor (C#)

// https://docs.microsoft.com/en-us/aspnet/core/blazor/webassembly-lazy-load-assemblies?view=aspnetcore-5.0 // add a new project to your solution of the type “Razor Class Library” // and make sure that “Support pages and views” is UNselcted //

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A

Create a service

Flutter (Dart)

React (TypeScript)

cd src/features/languages mkdir entities mkdir services touch ./entities/TimelineEntryEntity.ts touch ./services/TimelineSorterServices.ts

TimelineEntryEntity.ts

export default class TimelineEntryEntity { constructor(name:string = '', year:number = 0) { this.name = name; this.year = year; } name:string = ''; year:number = 0; }

TimelineSorterServices.ts

import TimelineEntryEntity from '../entities/TimelineEntryEntity'; export default class TimelineSorterService { constructor() { // } numberSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { return a.year - b.year; } stringSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; } }

React (JavaScript)

cd src/languages mkdir services touch ./services/TimelineSorterServices.js

TimelineSorterServices.js

export function numberSorterService(a, b) { return a.year - b.year; } export function stringSorterService(a, b) { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; }

Angular (TypeScript) CLI

# https://angular.io/cli/generate ng generate class languages/entities/timelineEntry --type entity ng generate service languages/services/timelineSorter

timeline-entry.entity.ts

export default class TimelineEntryEntity { constructor(name:string = '', year:number = 0) { this.name = name; this.year = year; } name:string = ''; year:number = 0; }

timeline-sorter.service.ts

import { Injectable } from '@angular/core'; import TimelineEntryEntity from '../entities/timeline-entry.entity'; // requires a class decorated with @Injectable // to use dependency injection @Injectable({ providedIn: 'root' }) export default class TimelineSorterService { constructor() { // } numberSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { return a.year - b.year; } stringSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; } }

Vue (TypeScript)

cd src/languages mkdir entities mkdir services touch ./entities/TimelineEntryEntity.ts touch ./services/TimelineSorterServices.ts

TimelineEntryEntity.ts

export default class TimelineEntryEntity { constructor(name:string = '', year:number = 0) { this.name = name; this.year = year; } name:string = ''; year:number = 0; }

TimelineSorterServices.ts

import TimelineEntryEntity from '../entities/TimelineEntryEntity'; export default class TimelineSorterService { constructor() { // } numberSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { return a.year - b.year; } stringSorter(a:TimelineEntryEntity, b:TimelineEntryEntity):number { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; } }

Vue (JavaScript)

cd src/languages mkdir services touch ./services/TimelineSorterServices.js

TimelineSorterServices.js

export function numberSorterService(a, b) { return a.year - b.year; } export function stringSorterService(a, b) { const nameA = a.name.toUpperCase(); const nameB = b.name.toUpperCase(); if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0; }

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A

Create a component

Flutter (Dart)

timeline.dart

import 'package:flutter/material.dart'; import 'package:spa_sample_flutter/SortBy.dart'; import 'package:spa_sample_flutter/entities/TimelineEntryEntity.dart'; // widgets that change their content must inherit from StatefulWidget class Timeline extends StatefulWidget { // component attributes are passed in the constructor Timeline({Key key, this.title, @required this.entries, this.sortBy}) : super(key: key); final String title; final List<TimelineEntryEntity> entries; final SortBy sortBy; @override _TimelineState createState() => _TimelineState(); } class _TimelineState extends State { List<TimelineEntryEntity> _sortedEntries; void sort() { var sortedEntries = this.widget.entries; // setState triggers a rebuild setState(() { _sortedEntries = sortedEntries; }); } // view is built with dart language syntax @override Widget build(BuildContext context) { return ListView.builder( padding: const EdgeInsets.all(8), itemCount: this._sortedEntries.length, itemBuilder: (BuildContext context, int index) { var entry = this._sortedEntries[index]; return TextButton( /*style: TextButton.styleFrom( textStyle: const TextStyle(fontSize: 20), ),*/ onPressed: () { Navigator.pushNamed(context, 'languages/${entry.name}') // arguments: ScreenArguments(entry.name) }, child: const Text('${entry.name} - ${entry.year}') ), } ); } }

React (TypeScript)

React (JavaScript)

cd src/features/languages mkdir components touch ./components/Timeline.jsx

Timeline.jsx

import { numberSorterService, stringSorterService } from '../services/TimelineSorterServices'; // component markup attributes are passed in the props parameter // The class name is the markup tag name export default function Timeline(props) { let title = 'Timeline'; let _entries = []; let sortBy = 'year'; function setEntries(entries) { let sortedEntries = []; switch (sortBy) { case 'name': sortedEntries = entries.sort(stringSorterService); break; case 'year': sortedEntries = entries.sort(numberSorterService); break; default: sortedEntries = entries; break; } _entries = sortedEntries; } setEntries(props.entries); // React uses a file type called JSX, that mixes JavaScript/TypeScript with markup // It is not the same as HTML and we can interpolate scripts in the markup inside {} // so comments for instance, must be added with {/* */} instead of <!-- --> // <> is a shortcut for <React.Fragment> https://reactjs.org/docs/fragments.html // It is a container tag that will not generate any html tag in the final rendering // JSX requires you to return only a sinqle node from a component function, so these // will come in handy return ( <> {/* Binding in JSX is done with the sintax {variable} */} <h2>{title}</h2> {/* Adding or removing elements is done by mixing JavaScript with markup https://reactjs.org/docs/conditional-rendering.html https://reactjs.org/docs/lists-and-keys.html */} { _entries.length > 0 && <ul> { _entries.map((entry) => <li key={entry.name}>{/* Each item in a loop must have an unique key */} {/* Link is used for internal links */} <Link to={`languages/${entry.name}`}>{entry.name} - {entry.year}</Link> </li> )} </ul> } </> ); }

Angular (TypeScript) CLI

# https://angular.io/cli/generate ng generate component timeline --module languages

timeline.component.ts

import { Component, Input, OnInit } from '@angular/core'; import TimelineEntryEntity from '../../entities/timeline-entry.entity'; import TimelineSorterService from '../../services/timeline-sorter.service'; // components classes must be decorated with @Component // provide a markup tag name that uses this class // and separated files for template and style // or inline strings equivalents // template: '
...
' and styles: ['div {...}', 'p {...}'] @Component({ selector: 'timeline', templateUrl: './timeline.component.html', styleUrls: ['./timeline.component.scss'] }) export class TimelineComponent implements OnInit { // component markup attributes must be decorated with @Input @Input() title:string = 'Timeline'; // for getter and setters, only one of the pair is decorated @Input() get entries():Array<TimelineEntryEntity> { return this._entries; } set entries(entries:Array<TimelineEntryEntity>) { let sortedEntries:Array<TimelineEntryEntity> = []; switch (this.sortBy) { case 'name': sortedEntries = entries.sort(this.timelineSorterService.stringSorter); break; case 'year': sortedEntries = entries.sort(this.timelineSorterService.numberSorter); break; default: sortedEntries = entries; break; } this._entries = sortedEntries; } @Input() sortBy:string = 'year'; private _entries:Array<TimelineEntryEntity> = [] constructor( // while this is a TypeScript shortcut to define a inputProperty // Angular highjacked it for dependency injection private timelineSorterService:TimelineSorterService ) { } // ngOnInit is part of the OnInit interface, for components lifecycle events ngOnInit(): void { } }

timeline.component.html

<!-- <ng-container> is a container tag that will not generate any html tag in the final rendering --> <ng-container> <!-- Angular has three differet binding syntaxes {{inputProperty}} is for binding @Input properties inside text nodes [inputProperty] is for binding @Input properties in attributes and (observable) is for binding @Output events also in attributes --> <h2>{{ title }}</h2> <!-- *ng... are structural directives, they may change the DOM tree, adding or removing elements. Structural directives always begin with * and have additional options https://angular.io/api/common/NgIf https://angular.io/api/common/NgForOf --> <ul *ngIf='entries'> <li *ngFor='let entry of entries'> <!-- routerLink is used for internal links --> <a [routerLink]='entry.name'> {{ entry.name }} - {{ entry.year }} </a> </li> </ul> </ng-container>

Vue (TypeScript)

// follow progress on class based components // right now the documentation seems out of date // and fragmented

Vue (JavaScript)

cd src/languages touch ./languages/Timeline.vue touch ./languages/Timeline.js

Timeline.js

import { stringSorterService, numberSorterService } from '../services/TimelineSorterServices' // https://v3.vuejs.org/guide/single-file-component.html // to use a proper class, a TypeScript project is required export default { name: 'Timeline', // inheritAttrs: false, props: { title: { required: false, type: String, default: 'Timeline' }, entries: Array, sortBy: { required: false, type: String, default: 'year' } }, computed: { sortedEntries: { get() { // creating a setter for this setter throws an error! // So the sorting logic needs to be called on the getter 🤮 let sortedEntriesTemp = []; switch (this.sortBy) { case 'name': sortedEntriesTemp = this.entries.sort(stringSorterService); break; case 'year': sortedEntriesTemp = this.entries.sort(numberSorterService); break; default: sortedEntriesTemp = this.entries; break; } return sortedEntriesTemp; } } }, methods: { hasEntries() { return this.entries; } } }

Timeline.vue

<!-- <template> is a container tag for the markup --> <template> <!-- Vue has three differet binding syntaxes {{property}} is for binding properties inside text nodes :property is for binding properties in attributes and @event is for binding events also in attributes There are longer alternate syntaxes, but why would you want to use them? --> <h2>{{ title }}</h2> <!-- Adding or removing elements is done with directives such as v-if and v-for. https://v3.vuejs.org/guide/conditional.html https://v3.vuejs.org/guide/list.html --> <ul v-if='hasEntries()'> <!-- we need to use a separate getter 'sortedEntries' for the sorting to work --> <li v-for='entry in sortedEntries' :key='entry.name'> <!-- router-link is used for internal links --> <router-link :to='entry.name'> {{ entry.name }} - {{ entry.year }} </router-link> </li> </ul> </template> <!-- Vue documentation usually puts everything in a single file But at least they did not forbid us from separating them like this --> <script src='./Timeline.js'></script>

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A

Use a component

Flutter (Dart)

Languages.dart

import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:spa_sample_flutter/SortBy.dart'; import 'package:spa_sample_flutter/Timeline.dart'; import 'package:spa_sample_flutter/entities/TimelineEntryEntity.dart'; // import 'Timeline.dart'; class Languages extends StatelessWidget { Languages({Key key, this.timelineTitle}) : super(key: key); final String timelineTitle; final List<TimelineEntryEntity> timelineEntries = [ // new keyword is optional new TimelineEntryEntity(name: 'Fortran', year: 1957), new TimelineEntryEntity(name: 'Lisp', year: 1958), new TimelineEntryEntity(name: 'Algol', year: 1958), new TimelineEntryEntity(name: 'Cobol', year: 1959), new TimelineEntryEntity(name: 'Simula', year: 1962), new TimelineEntryEntity(name: 'Basic', year: 1964), new TimelineEntryEntity(name: 'Pascal', year: 1970), new TimelineEntryEntity(name: 'C', year: 1972), new TimelineEntryEntity(name: 'Smalltalk 72', year: 1972), new TimelineEntryEntity(name: 'Ada', year: 1980), new TimelineEntryEntity(name: 'Smalltalk 80', year: 1980), new TimelineEntryEntity(name: 'Objective-C', year: 1984), new TimelineEntryEntity(name: 'C++', year: 1985), new TimelineEntryEntity(name: 'Object Pascal', year: 1986), new TimelineEntryEntity(name: 'Erlang', year: 1986), new TimelineEntryEntity(name: 'Python', year: 1990), new TimelineEntryEntity(name: 'Haskell', year: 1990), new TimelineEntryEntity(name: 'Java', year: 1995), new TimelineEntryEntity(name: 'JavaScript', year: 1995), new TimelineEntryEntity(name: 'Php', year: 1995), new TimelineEntryEntity(name: 'Ruby', year: 1995), new TimelineEntryEntity(name: 'ActionScript', year: 1998), new TimelineEntryEntity(name: 'Visual Basic 6', year: 1998), new TimelineEntryEntity(name: 'C#', year: 2000), new TimelineEntryEntity(name: 'F#', year: 2005), new TimelineEntryEntity(name: 'Go', year: 2009), new TimelineEntryEntity(name: 'Rust', year: 2010), new TimelineEntryEntity(name: 'Kotlin', year: 2011), new TimelineEntryEntity(name: 'Dart', year: 2011), new TimelineEntryEntity(name: 'TypeScript', year: 2012), new TimelineEntryEntity(name: 'Swift', year: 2014) ]; @override Widget build(BuildContext context) { SystemChrome.setApplicationSwitcherDescription( ApplicationSwitcherDescription( label: timelineTitle, primaryColor: Theme.of(context).primaryColor.value, ) ); return Scaffold( body: Timeline( title: 'Programming languages', entries: this.timelineEntries, sortBy: SortBy.name, )); } }

React (TypeScript)

React (JavaScript) - languages.jsx

import React, { useEffect } from 'react'; // import the component import Timeline from './components/Timeline'; export default function Languages() { let timelineTitle = 'Programming languages'; let timelineEntries = [ { name: 'Fortran', year: 1957 }, { name: 'Lisp', year: 1958 }, { name: 'Algol', year: 1958 }, { name: 'Cobol', year: 1959 }, { name: 'Simula', year: 1962 }, { name: 'Basic', year: 1964 }, { name: 'Pascal', year: 1970 }, { name: 'C', year: 1972 }, { name: 'Smalltalk 72', year: 1972 }, { name: 'Ada', year: 1980 }, { name: 'Smalltalk 80', year: 1980 }, { name: 'Objective-C', year: 1984 }, { name: 'C++', year: 1985 }, { name: 'Object Pascal', year: 1986 }, { name: 'Erlang', year: 1986 }, { name: 'Python', year: 1990 }, { name: 'Haskell', year: 1990 }, { name: 'Java', year: 1995 }, { name: 'JavaScript', year: 1995 }, { name: 'Php', year: 1995 }, { name: 'Ruby', year: 1995 }, { name: 'ActionScript', year: 1998 }, { name: 'Visual Basic 6', year: 1998 }, { name: 'C#', year: 2000 }, { name: 'F#', year: 2005 }, { name: 'Go', year: 2009 }, { name: 'Rust', year: 2010 }, { name: 'Kotlin', year: 2011 }, { name: 'Dart', year: 2011 }, { name: 'TypeScript', year: 2012 }, { name: 'Swift', year: 2014 } ]; // component rendering side effects use the useEffect() function // it cannot be placed inside an if (must always be called), // but you can still put the if inside the callback if you need to useEffect(() => { document.title = 'Languages'; }); // use the tag according to the class name return ( <Timeline title={timelineTitle} entries={timelineEntries} sortBy='name' ></Timeline > ); }

Angular - languages.component.ts

import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import TimelineEntryEntity from '../entities/timeline-entry.entity'; @Component({ selector: 'app-languages', templateUrl: './languages.component.html', styleUrls: ['./languages.component.scss'] }) export class LanguagesComponent implements OnInit { timelineTitle:string = 'Programming languages'; timelineEntries:Array<TimelineEntryEntity> = [ new TimelineEntryEntity('Fortran', 1957), new TimelineEntryEntity('Lisp', 1958), new TimelineEntryEntity('Algol', 1958), new TimelineEntryEntity('Cobol', 1959), new TimelineEntryEntity('Simula', 1962), new TimelineEntryEntity('Basic', 1964), new TimelineEntryEntity('Pascal', 1970), new TimelineEntryEntity('C', 1972), new TimelineEntryEntity('Smalltalk 72', 1972), new TimelineEntryEntity('Ada', 1980), new TimelineEntryEntity('Smalltalk 80', 1980), new TimelineEntryEntity('Objective-C', 1984), new TimelineEntryEntity('C++', 1985), new TimelineEntryEntity('Object Pascal', 1986), new TimelineEntryEntity('Erlang', 1986), new TimelineEntryEntity('Python', 1990), new TimelineEntryEntity('Haskell', 1990), new TimelineEntryEntity('Java', 1995), new TimelineEntryEntity('JavaScript', 1995), new TimelineEntryEntity('Php', 1995), new TimelineEntryEntity('Ruby', 1995), new TimelineEntryEntity('ActionScript', 1998), new TimelineEntryEntity('Visual Basic 6', 1998), new TimelineEntryEntity('C#', 2000), new TimelineEntryEntity('F#', 2005), new TimelineEntryEntity('Go', 2009), new TimelineEntryEntity('Rust', 2010), new TimelineEntryEntity('Kotlin', 2011), new TimelineEntryEntity('Dart', 2011), new TimelineEntryEntity('TypeScript', 2012), new TimelineEntryEntity('Swift', 2014) ]; constructor(private titleService: Title) { } ngOnInit(): void { // Angular has a dedicated service for changing the browser title // allegedly to prevent issues if running angular as a native app // for instance this.titleService.setTitle('Languages'); } }

languages.component.html

<!-- use the tag without the “app-” prefix --> <timeline [title]='timelineTitle' [entries]='timelineEntries' sortBy='name' ></timeline >

languages.module.ts

// this will be created automatically import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { LanguagesRoutingModule } from './languages-routing.module'; // this will be added automatically // import components import { LanguagesComponent } from './components/languages.component'; import { TimelineComponent } from './components/timeline/timeline.component'; // this will be added automatically // declare components in the module scope @NgModule({ declarations: [ LanguagesComponent, TimelineComponent ], imports: [ CommonModule, LanguagesRoutingModule ] }) export class LanguagesModule { }

Vue (TypeScript) - Languages.ts

// follow progress on class based components // right now the documentation seems out of date // and fragmented

Languages.vue

<template> <Timeline :title='timelineTitle' :entries='timelineEntries' sortBy='name' /> </template> <script src='./Languages.ts' lang='ts'></script>

Vue (JavaScript) - Languages.js

// import the component, // even though we will not reference it from JavaScript! import Timeline from './Timeline.vue' export default { name: 'Languages', components: { Timeline }, data() { return { timelineTitle: 'Programming languages', timelineEntries: [ { name: 'Fortran', year: 1957 }, { name: 'Lisp', year: 1958 }, { name: 'Algol', year: 1958 }, { name: 'Cobol', year: 1959 }, { name: 'Simula', year: 1962 }, { name: 'Basic', year: 1964 }, { name: 'Pascal', year: 1970 }, { name: 'C', year: 1972 }, { name: 'Smalltalk 72', year: 1972 }, { name: 'Ada', year: 1980 }, { name: 'Smalltalk 80', year: 1980 }, { name: 'Objective-C', year: 1984 }, { name: 'C++', year: 1985 }, { name: 'Object Pascal', year: 1986 }, { name: 'Erlang', year: 1986 }, { name: 'Python', year: 1990 }, { name: 'Haskell', year: 1990 }, { name: 'Java', year: 1995 }, { name: 'JavaScript', year: 1995 }, { name: 'Php', year: 1995 }, { name: 'Ruby', year: 1995 }, { name: 'ActionScript', year: 1998 }, { name: 'Visual Basic 6', year: 1998 }, { name: 'C#', year: 2000 }, { name: 'F#', year: 2005 }, { name: 'Go', year: 2009 }, { name: 'Rust', year: 2010 }, { name: 'Kotlin', year: 2011 }, { name: 'Dart', year: 2011 }, { name: 'TypeScript', year: 2012 }, { name: 'Swift', year: 2014 } ] } }, created() { document.title = 'Languages'; } }

Languages.vue

<template> <Timeline :title='timelineTitle' :entries='timelineEntries' sortBy='name' /> </template> <script src='./Languages.js'></script>

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

Angular cleanly separates concers here, and the boilerplate parts of configuring the dependencies in the module are done automatically by the CLI.

React is pretty clean and straightforward in this simple example, but again, putting both the logic and the view in a single file proves to be hard to manage when your code grows. Separating the rendering portion is not a common practice in React, and would mean more varibles passing in function calls.

Vue seems to make things more confusing. Custom tags centric frameworks such as .Net Web Forms or JSP imported new tags in the markup if needed, which is were they would be used anyway. But Vue moves this to the code portion, where there’s no mention of it. To make matters worse, the .vue file lists the markup first, and the code later. Maybe we can change the order, but that is not what the examples in the official documentation do. This makes a tiny more sense if everything is crammed up in a single file, but again, I don’t believe that is a good practice either.

Use state management (redux like)

Flutter (Dart)

React (TypeScript)

React (JavaScript)

languageSlice.js

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; // for async operations such as an http request // redux gives us a helper called createAsyncThunk // that emits events during the execution and // play nice with reducers (applicaton state mutators) const languageReduxSliceKey = 'language'; export const languageAsyncThunk = createAsyncThunk( `${languageReduxSliceKey}/get`, async (payload) => { const response = await new Promise((resolve, reject) => { resolve({ title: 'some title', description: 'some description' }); }); return response; } ); // redux also gives us a helper called createSlice, that simplifies // operating on a branch (or slice) of the application state tree const languageSlice = createSlice({ name: languageReduxSliceKey, initialState: { status: 'idle', title: '', description: '' }, // the regular (non thunk event) reducers are registered here reducers: { }, // the thunk event reducers are registered here extraReducers: builder => { builder .addCase(languageAsyncThunk.pending, (state, action) => { state.status = 'loading'; }) .addCase(languageAsyncThunk.fulfilled, (state, action) => { const payload = action.payload; state.title = payload.title; state.description = payload.description; state.status = 'idle'; }) .addCase(languageAsyncThunk.rejected, (state, action) => { state.status = 'error'; }); } }); // we export all reducers like this export default languageSlice.reducer; // and the selectors for the branch (or slice) // of the applicaton state tree like this export const selectLanguage = state => state[languageReduxSliceKey];

store.js

import { configureStore } from '@reduxjs/toolkit'; import languageReducer from '../features/languages/languageSlice'; export default configureStore({ reducer: { language: languageReducer }, });

Language.jsx

// redux import { useSelector, useDispatch } from 'react-redux'; import { languageAsyncThunk, selectLanguage } from '../languageSlice'; export default function Language(props) { // read the current redux tree state branch const language = useSelector(selectLanguage); let languageTitle = language.title ?? ''; let languageDescription = language.description ?? ''; // push a new state for a branch const dispatch = useDispatch(); async function loadLanguage(language) { const resultAction = await dispatch(languageAsyncThunk({ language: language })); if (languageAsyncThunk.fulfilled.match(resultAction)) { languageTitle = language.title; languageDescription = language.description; } else { // resultAction.payload // resultAction.error } } loadLanguage('JavaScript'); // TODO check a flag to prevent multiple calls // render the current state return ( <> {languageTitle} {languageDescription} </> ); } // https://redux.js.org/recipes/implementing-undo-history

Angular (TypeScript) CLI

Vue (TypeScript)

Vue (JavaScript)

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A

Forms

Flutter (Dart)

// https://flutter.dev/docs/cookbook/forms/validation

React (TypeScript)

React (JavaScript)

Angular (TypeScript) CLI

// https://angular.io/guide/reactive-forms

Vue (TypeScript)

Vue (JavaScript)

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A

Focus management

Flutter (Dart)

// https://api.flutter.dev/flutter/widgets/Focus-class.html // https://flutter.dev/docs/cookbook/forms/focus

React (TypeScript)

React (JavaScript)

Angular (TypeScript) CLI

// https://angular.io/guide/reactive-forms

Vue (TypeScript)

Vue (JavaScript)

Blazor (C#)

Yew (Rust)

Seed (Rust)

Vugu (Go)

Kotlin/JS

Web Components

A