Vue: A Better React?

Norfolk.js

Norfolk.js - April 2017

Matt Campbell

@codingcampbell

Grow

thisisgrow.com

Clients

0 Day(s) Since The Last JavaScript Framework

risingstars2016.js.org

What is Vue?

A progressive, incrementally-adoptable JavaScript framework for building UI on the webgithub

Influenced by many frameworks, adapting and (and even improving) their features.

Spoiler alert:

Vue is a smaller, practical take on React with less opinions, more features out of the box, and a more powerful reactivity model.

Thank You

Well, that was easy.

Any questions?

grw.to/vue

Who makes Vue?

Primarily written by Evan You, previously a MeteorJS contributor.

Evan’s work on Vue is crowd-funded on Patreon for over $10,000/mo

The bus-factor is unfortunately very low (85% commits by Evan):

Comparing to other frameworks

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:

Quick Overview: Terminology

Quick Overview: Component Flow

Getting Started with Vue

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)

The .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>

Scoped Styles In .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;
}

Vue’s templating language

<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:

But it does have some interesting things

Custom components in templates

<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:

JSX plugin for Vue

{ !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:

Custom components in JSX

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:

Creating a component

export 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:

Creating a component

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
};

Stateless functional components

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>;
  }
});

Stateless functional components

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
};

Lifecycles

export default Vue.extend({
  beforeMount() {},   // React: componentWillMount()

  mounted() {},       // React: componentDidMount()

  beforeDestroy() {}, // React: componentWillUnmount()

  destroyed() {},     // React: componentDidUnmount()

  render() {},        // React: render()
});

Notably missing:

Reactivity Model

export default Vue.extend({
  data() {
    return {
      firstName: 'Matt',
      lastName: 'Campbell',
    };
  },

  render(h) {
    return <h1>{this.firstName}</h1>;
  }
});

Notes:

Reactivity Model

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;
}

Nested Reactivity

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()

Nested Reactivity

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' ...

Nested Reactivity

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' }
});

Computed Properties

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:

Computed Properties

const App = React.createClass({
  getInitialState() {
    return { firstName: 'Matt', lastName: 'Campbell' };
  },

  getFullName() {
    return this.state.firstName + ' ' + this.state.lastName;
  },

  render() { return <h1>{this.getFullName()}</h1> }
});

Notes:

Watchers

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:

Watchers

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
    }
  }
});

Cross-component Reactivity

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:

Cross-component Reactivity

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.

Centralized State

Centralized State: Component as Store

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

Centralized State: TodoList

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>;
  }
});

Centralized State: Todo

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>;
  }
});

Actual Centralized State: Vuex

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:

Thank You

For real this time

Any questions?