To provide a modern web experience to our users, we need to adopt a JavaScript library. I am currently trying to decide between Vue.js and React. Both are libraries rather than frameworks, which can easily be integrated into existing pages. Vue.js is easy to pick up, uses a familiar CSS/HTML/JS structure, and does not lean towards newer JavaScript variations, but it is very new, maintained by one person, and still relatively unknown. React has a difficult learning curve, leans towards EcmaScript 2016, and encourages users to put everything, including CSS and HTML, into their JavaScript code, but it is the current leader, has a robust ecosystem and tool kit, and is backed by Facebook.
Angular (and Ember) are frameworks that are intended more for websites that are built as Single Page Applications. Meteor is a complete web stack that uses NodeJS for its backend. Thus, neither of these are applicable for our needs.
Just two years ago, in 2014, the primary JavaScript SPA/UI library decision was between Angular, Ember, and Backbone. Last year in 2015, Angular appeared to be the clear winner: "[I]f you’re hoping to find safety in numbers, your choice is clear: AngularJS. It’s the clear community winner. ..." Now, in 2016, some are claiming that React has "won the client-side war", while others predict that "Vue.js will sky-rocket in popularity in 2016".
As it stands, I am torn between Vue.js and React. Unlike Angular, which is a framework, Vue.js and React are both very flexible and thin extendable libraries, focusing primarily on the "view layer" in the MVC paradigm, which makes them easy to drop into existing pages for additional functionality. The main difference is that Vue.js feels very natural to use but is still relatively new and unknown and primarily maintained by a single person; while React has a higher learning curve and somewhat awkwardly tries to do everything in JavaScript and JSX (its own language extension), is currently the crowd favorite, has an extensive ecosystem, and is backed by Facebook.
For the sake of completion, I do want to cover Angular, even though I don't think it should be up for consideration. Part of the reason Angular fell from favor Google's previous announcement that Angular 2 would be using a very different API and offer little in the way of an upgrade path, in addition to its embrace of TypeScript.
It would be difficult to use Angular in a maintainable fashion without npm as shown by their boilerplate HTML file (from their quickstart guide):
<html>
<head>
<title>Angular 2 QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- 1. Load libraries -->
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<!-- 2. Configure SystemJS -->
<script src="systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
In this example, shim.min.js handles cross-browser support, zone.js handles events, Reflect.js handles dependency injection, and system.src.js and systemjs.config.js handle the loading of modules.
Here is an example of the systemjs.config.js file where everything gets loaded:
/**
* System configuration for Angular 2 samples
* Adjust as necessary for your application needs.
*/
(function(global) {
// map tells the System loader where to look for things
var map = {
'app': 'app', // 'dist',
'@angular': 'node_modules/@angular',
'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api',
'rxjs': 'node_modules/rxjs'
};
// packages tells the System loader how to load when no filename and/or no extension
var packages = {
'app': { main: 'main.js', defaultExtension: 'js' },
'rxjs': { defaultExtension: 'js' },
'angular2-in-memory-web-api': { main: 'index.js', defaultExtension: 'js' },
};
var ngPackageNames = [
'common',
'compiler',
'core',
'forms',
'http',
'platform-browser',
'platform-browser-dynamic',
'router',
'router-deprecated',
'upgrade',
];
// Individual files (~300 requests):
function packIndex(pkgName) {
packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' };
}
// Bundled (~40 requests):
function packUmd(pkgName) {
packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
}
// Most environments should use UMD; some (Karma) need the individual index files
var setPackageConfig = System.packageWithIndex ? packIndex : packUmd;
// Add package entries for angular packages
ngPackageNames.forEach(setPackageConfig);
var config = {
map: map,
packages: packages
};
System.config(config);
})(this);
This file loads main.js (which is transpiled from main.ts):
import { bootstrap } from '@angular/platform-browser-dynamic';
import { AppComponent } from './app.component';
bootstrap(AppComponent);
Which in turn loads app.component.js (which is transpiled from app.component.js):
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: '<h1>My First Angular 2 App</h1>'
})
export class AppComponent { }
As shown here, one of the turns that Angular made was to take a component approach to web application development, where everything is placed into one file and turned into a reusable custom HTML element.
React also takes a component approach, but since it is just a library, the number of files that must be included are not as vast, as shown in this example (from their quickstart):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="build/react.js"></script>
<script src="build/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
</script>
</body>
</html>
In this example react.js is the core library and react-dom.js adds features for use in the browser (React is designed to be used in a number of environments, inlcuding mobile with React Native, which is part of the reason they use the device agnostice JavaScript language extension JSX). Babel is included to transpile the JSX (as well as any ECMAScript 2016 that is used).
Here is a more complete example that allows the user to search Flickr images (with a working demo on JSFiddle):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://fb.me/react-15.2.1.js"></script>
<script src="https://fb.me/react-dom-15.2.1.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.10.3/babel.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js" integrity="sha256-Qw82+bXyGq6MydymqBxNPYTaUXXq7c8v3CwiYwLLNXU=" crossorigin="anonymous"></script>
</head>
<body>
<div id="app"></div>
<script type="text/babel">
var Input = React.createClass({
getInitialState: function() {
return {
value: ""
};
},
onChange: function(e) {
this.setState({ value: e.target.value });
},
onKeyDown: function(e) {
if (e.keyCode == 13) {
this.props.localHandleChange(this.state.value);
}
},
localHandleSubmit: function(e) {
e.preventDefault();
this.props.localHandleChange(this.state.value);
this.setState({ value: '' });
},
render: function() {
return (
<div>
<input
type="text"
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
<button onClick={this.localHandleSubmit}>{"Search"}</button>
</div>
);
}
});
var Output = React.createClass({
render: function() {
return (
<p>{this.props.value}</p>
);
}
});
var Flickr = React.createClass({
getInitialState: function() {
return {
pictures: []
};
},
componentDidMount: function() {
var pictures = [];
if (this.props.data.photos.photo.length > 0) {
var component = this;
this.props.data.photos.photo.forEach(function(element) {
pictures.push(<div>{element.id}</div>);
});
}
this.setState({
pictures: pictures
});
},
render: function() {
var pictures = [];
if (this.props.data.photos.photo.length > 0) {
console.log('werkin');
this.props.data.photos.photo.forEach(function(element) {
pictures.push(<div key={element.id}><a href={"https://www.flickr.com/photos/" + element.owner + "/" + element.id} target={"_blank"}><img src={"https://farm" + element.farm + ".staticflickr.com/" + element.server + "/" + element.id + "_" + element.secret + "_m.jpg"} /></a></div>);
});
}
return (
<div>{pictures}</div>
);
}
});
var App = React.createClass({
getInitialState: function() {
return {
value: '',
data: {
photos: {
photo: {}
}
}
};
},
localHandleChange: function(value) {
this.setState({ value: value});
var component = this;
console.dir(value);
var searchUrl = "https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=7d41908eaef7c9e97c3f15a2ac6ef700&text=" + value + "&format=json&nojsoncallback=1";
$.get(searchUrl, function(data) {
this.setState({
data: data
});
}.bind(this));
},
render: function() {
return (
<div>
<Input localHandleChange={this.localHandleChange} />
<Output value={this.state.value} />
<Flickr data={this.state.data} />
</div>
)
}
});
ReactDOM.render(<App />, document.getElementById("app"));
</script>
</body>
</html>
The pattern here is to use "var Input = React.createClass" and the like to create components, combine and output them in "var App = React.createClass" using the render function, then bind them to the page using "ReactDOM.render".
When I initially looked at React way back when I was unable to make heads or tails of it. It was only after working with Vue.js that I was able to wrap my mind around React and this still took several days of intense experimentation. This difficulty had less to do with its use of JSX than it did with React's insistence on using one-way data binding and avoidance of state. React does allow for state to be passed among components using "props", but most applications use the Redux library to manage state -- neither of which are plainly intuitive.
For example, the onChange function that is used by the Input component is actually passed in from the App component as a prop, rather than being defined in the Input component itself. The reason for this is that, without Redux, state should be managed in the application's root parent component, which in this case is App, and using a callback function is one of the methods of giving the function access to the parent component's state.
A number of features set Vue.js apart:
- Vue.js uses a standard CSS/HTML/JS approach to its applications.
- Vue.js does not lean in the direction of newer JavaScript variations that require transpiling, such as TypeScript with Angular or JSX and ECMAScript 2016 with React.
- Since Vue.js just covers the view layer and only targets the browser, it can provide its functionality in a minimal number of includes.
These features are highlighted in the sample below, which replicates the React Flickr search from above (with a demo on JSFiddle):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://npmcdn.com/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/vue.resource/0.9.3/vue-resource.min.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="value" v-on:keyup.enter="flickrSearch" placeholder="Search Flickr">
<button v-on:click="flickrSearch">Search Flickr</button>
<p v-for="photo in flickr.photos.photo">
<a href="{{ 'https://www.flickr.com/photos/' + photo.owner + '/' + photo.id }}" target="_blank"><img src="{{'https://farm' + photo.farm + '.staticflickr.com/' + photo.server + '/' + photo.id + '_' + photo.secret + '_m.jpg'}}"
/></a>
</p>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
value: '',
flickr: ''
},
methods: {
flickrSearch: function() {
var searchUrl = "https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=7d41908eaef7c9e97c3f15a2ac6ef700&text=" + this.value + "&format=json&nojsoncallback=1";
this.$http.get(searchUrl).then((response) => {
this.flickr = JSON.parse(response.data);
}, (response) => {
// error callback
});
}
}
})
</script>
</body>
</html>