0
Day(s) Since The Last JavaScript FrameworkA progressive, incrementally-adoptable JavaScript framework for building UI on the web – github
Influenced by many frameworks, adapting and (and even improving) their features.
Vue is a smaller, practical take on React with less opinions, more features out of the box, and a more powerful reactivity model.
Primarily written by Evan You, previously a MeteorJS contributor.
Evan’s work on Vue is crowd-funded on Patreon for over $10,000/mo
Vue’s documentation has a detailed comparison to other popular frameworks. Check it out if you’re looking for in-depth details and benchmarks.
In the meantime, this talk specifically compares Vue to React in terms of technology and development experience.
Starting with the ever-important download size:
Include the runtime build as a script:
<div id="app"></div>
<script src="https://unpkg.com/vue"></script>
<script>
new Vue({
el: '#app',
template: '<h1>Hello World</h1>'
});
</script>
Or you can use the CLI:
npm install -g vue-cli
vue init webpack hello
cd hello
npm install
This uses the webpack template (there are others, like browserify)
.vue
Single File Component<template>
<div class="example">
{{ msg }}
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
msg: 'Hello world!'
};
}
}
</script>
<style lang="scss">
.example {
color: red;
}
</style>
.vue
Format<style lang="scss">
.example {
color: red;
}
</style>
becomes:
.example {
color: red;
}
<style lang="scss" scoped>
.example {
color: red;
}
</style>
becomes:
.example[data-v-71418bfb] {
color: red;
}
<section class="main" v-show="todos.length" v-cloak>
<ul class="todo-list">
<li v-for="todo in filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }">
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
Things I don’t like:
v-show
is awkward)class
vs :class
vs @dblclick
)<template>
<div class="example">
<hello></hello>
</div>
</template>
<script>
import Hello from './components/Hello';
export default {
name: 'app',
components: { Hello } // or explicitly name: { hello: Hello }
}
</script>
Things I don’t like:
{ !todos.length ? null: <section class="main">
<ul class="todo-list">
{ filteredTodos.map(todo =>
<li
key={todo.id}
class={['todo', {completed: todo.completed, editing: todo === editedTodo}]}
<div class="view">
<input
class="toggle"
type="checkbox"
onChange={e => todo.completed = e.target.checked}>
<label onDblclick={() => this.editTodo(todo)}>{todo.title}</label>
<button class="destroy" onClick={() => removeTodo(todo)}></button>
)}
}
Vue maintains an official JSX plugin for Babel.
Some disadvantages:
v-model
(manual onChange
is needed)import Hello from './components/Hello';
export default Vue.extend({
name: 'app',
render(h) {
return <div class="example">
<Hello/>
</div>;
// h('div', {class: 'example'}, [Hello])
// React.createElement('div', {className: 'example'}, [Hello])
}
});
Differences from Vue’s templates:
h()
calls, no @jsx
pragmaexport default Vue.extend({
props: { // React: getDefaultProps()
name: { type: String, required: true },
age: { type: Number, default: 29 },
},
data() { // React: getInitialState()
return {
energy: 0,
};
},
render(h) {
return <span>{this.name} is {this.energy >= 50 ? 'awake' : 'sleepy'}</span>;
}
});
Notes:
React.createClass()
, e.g. methods are auto-bounddata
and props
exist in the same namespace, directly on this
export default class App {
constructor(props) {
this.props = props;
this.state = {
energy: 0,
};
}
render() {
return <span>
{this.props.name} is {this.state.energy >= 50 ? 'awake' : 'sleepy'}
</span>;
}
};
App.propTypes = {
name: React.PropTypes.string.isRequired,
age: React.PropTypes.number
};
App.defaultProps = {
age: 29
};
export default Vue.extend({
props: {
name: { type: String, required: true },
age: { type: Number, default: 29 },
},
functional: true, // <- this is the secret sauce
render(h, context) {
return <span>His name is {context.props.name} Paulson</span>;
}
});
export default function App(props) {
return <span>His name is {props.name} Paulson</span>;
}
App.propTypes = {
name: React.PropTypes.string.isRequired,
age: React.PropTypes.number
};
App.defaultProps = {
age: 29
};
export default Vue.extend({
beforeMount() {}, // React: componentWillMount()
mounted() {}, // React: componentDidMount()
beforeDestroy() {}, // React: componentWillUnmount()
destroyed() {}, // React: componentDidUnmount()
render() {}, // React: render()
});
Notably missing:
export default Vue.extend({
data() {
return {
firstName: 'Matt',
lastName: 'Campbell',
};
},
render(h) {
return <h1>{this.firstName}</h1>;
}
});
Notes:
render()
will only be called when firstName
is updatedlastName
(or anything else) will not trigger render()
export default class App {
constructor(props) {
this.props = props;
this.state = {
firstName: 'Matt',
lastName: 'Campbell',
};
}
render() {
return <span>
return <h1>{this.state.firstName}</h1>
</span>;
}
};
// Later ...
this.setState({ lastName: 'Paulson' }); // over-reacts with a redundant render()
// Unless you check manually:
shouldComponentUpdate(nextProps, nextState) {
return nextState.firstName !== this.state.firstName;
}
export default Vue.extend({
data() {
return {
person: {
firstName: 'Matt',
lastName: 'Campbell',
},
};
},
render(h) {
return <h1>{this.person.firstName}</h1>
}
});
// Later ...
this.person.firstName = 'Robert'; // triggers render()
this.person.lastName = 'Paulson'; // no render()
export default React.createClass({
getInitialState() {
return {
person: {
firstName: 'Matt',
lastName: 'Campbell',
},
};
},
render(h) {
return <h1>{this.state.person.firstName}</h1>
}
});
// Bad: overwrites entire person object
this.setState({
person: { lastName: 'Paulson' }
});
// Impossible:
this.setState({ 'person.firstName' ...
export default React.createClass({
getInitialState() {
return {
person: {
firstName: 'Matt',
lastName: 'Campbell',
},
};
},
render(h) {
return <h1>{this.state.person.firstName}</h1>
}
});
// Correct:
this.setState({
person: Object.assign({}, this.state.person, { firstName: 'Robert' })
});
// Or:
this.setState({
person: { ...this.state.person, firstName: 'Robert' }
});
export default Vue.extend({
data() {
return { firstName: 'Matt', lastName: 'Campbell' };
},
computed: {
fullName() { return this.firstName + ' ' + this.lastName; }
},
render(h) { return <h1>{this.fullName}</h1>; }
});
Notes:
render()
usesfirstName
or lastName
changesthis.fullName
is cached until one of the dependencies changeconst App = React.createClass({
getInitialState() {
return { firstName: 'Matt', lastName: 'Campbell' };
},
getFullName() {
return this.state.firstName + ' ' + this.state.lastName;
},
render() { return <h1>{this.getFullName()}</h1> }
});
Notes:
export default Vue.extend({
data: () => ({ progress: 0 }),
computed: {
progressMilestone() {
return Math.floor(progress / 10) * 10;
}
},
watch: {
progressMilestone(value) {
// this is triggered whenever the computed value changes
}
}
});
Notes:
export default React.createClass({
getInitialState: () => ({ progress: 0, progressMilestone: 0 }),
// Can't call `setState` inside `componentWillUpdate` so `progressMilestone` is briefly out-of-sync :(
componentDidUpdate(prevProps, nextState) {
if (prevState.progress !== this.state.progress) {
this.setState({ progressMilestone: Math.floor(this.state.progress / 10) * 10 });
}
},
componentWillUpdate(nextProps, nextState) {
if (nextState.progressMilestone !== this.state.progressMilestone) {
// And now our watch() has ended
}
}
});
const person = new Vue({
data() {
return {
firstName: 'Matt'
};
}
});
const OtherComponent = Vue.extend({
render() {
return <h1>{person.firstName}</h1>;
}
});
// Later:
person.firstName = 'Robert'; // OtherComponent.render()
Notes:
const Child = props => {
return <h1>{props.state.firstName}</h1>;
};
const App = React.createClass({
getInitialState: () => ({
firstName: 'Matt',
lastName: 'Campbell',
}),
render() {
return <div><Child state={this.state}/></div>;
}
});
This actually works, but isn’t the same, since App has to own Child in this case, it couldn’t be used to to share a centralized state.
This is a bit tricky in React due to some setState
rules. These are the kind of things you’d probably want to do with RxJS / BehaviorSubject
or similar.
const Store = new Vue({
data: () => ({
todos: [],
filter: null,
},
computed: {
activeTodos: self => self.todos.filter(todo => !todo.completed),
completedTodos: self => self.todos.filter(todo => todo.completed),
allTodosComplete self => self.completedTodos.length === self.todos.length,
},
created() {
this.$on('add-todo', this.addTodo);
this.$on('remove-todo', this.removeTodo);
this.$on('clear-completed', this.clearCompleted);
}
});
This centralized Store approach is even recommended by the guide
import Store from '../Store';
export default Vue.extend({
render() {
return <section class="main">
<ul class="todo-list">{ Store.todos.map((todo, index) =>
<Todo key={index + '-' + todo.task} todo={todo}/>
)}</ul>
</section>;
}
});
import Store from '../Store';
export default Vue.extend({
props: { todo: { required: true } },
data: () => ({ editing: false }),
methods: {
handleEdit(e) {
this.editing = true;
Vue.nextTick(() => { this.$refs.editInput.focus(); });
},
remove() {
Store.$emit('remove-todo', this.todo);
},
save() {
this.editing = false;
this.todo.task = this.$refs.editInput.value.trim();
},
},
render() {
return <li class={{completed: this.todo.completed, editing: this.editing}}>
<div class="view">
<label onDblclick={this.handleEdit}>{this.todo.task}</label>
<button class="destroy" onClick={this.remove}></button>
</div>
<input ref="editInput" class="edit" domPropsValue={this.todo.task} onBlur={this.save}/>
</li>;
}
});
export default {
state: { todos: [], filter: null },
mutations: {
addTodo(state, { task, completed }) {
state.todos.push({
id: String(Math.random()).slice(2),
task,
completed: !!completed,
});
},
removeTodo(state, todo) {
state.todos.splice(state.todos.indexOf(todo), 1);
},
editTodo(state, { todo, task }) {
todo.task = task;
},
},
getters: {
activeTodos: state => state.todos.filter(todo => !todo.completed),
completedTodos: state => state.todos.filter(todo => todo.completed),
allTodosComplete: (state, getters) => getters.completedTodos.length === state.todos.length,
}
};
Notes: