What’s the difference between all these?

One area of confusion when diving into Vue is distinguishing props, data, computed, and methods - particurlarly because all can be referenced directly on the component instance.

For example in React, you would access various properties like so:

  • props as this.props.propA
  • local state as this.state.localStateB.

However in Vue you access all of props, local state, methods, and computed properties directly scoped to the component instance:

  • props as this.propA
  • local state (aka data) as this.localStateB
  • computed as this.computedA
  • methods as this.methodC().

When to use each can take awhile to get a grasp of, so we’ll dive into more details and also explore reactivity and watchers.

Props

Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance

Props are similar to arguments you would pass a function. Your component renders based on the props it is given. A quick example is below:

<!-- Component definition with props -->
<script>
export default {
  props: ['message']
}
</script>

<template>
  <div>
    {{ message }}
  </div>
</template>

<!-- Usage -->
<Example message="Hello"/>

<!-- Rendered output -->
<div>
  Hello
</div>

If the data provided via props changes over time, it should either be managed by a parent component or you should initialize a local state property (aka data, more on that in the next section) with that prop:

<!-- Component definition with props -->
<script>
export default {
  props: ['initialCounter'],
  data () {
    return {
      counter: this.initialCounter
    }
  }
}
</script>

<template>
  <div>
    {{ counter }}
  </div>
</template>

<!-- Usage -->
<Counter :initialCounter="123"/>

<!-- Rendered output -->
<div>
  123`
</div>

To encourage one-way data flow, Vue will log a warning when you attempt to mutate a prop:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

There are a lot more you can do with props, such as setting default values, validations, and more. Read more about props in the official docs

Data

Vue’s data option is the local state of a component that can change over time. It is often consumed directly by that component or passed on to child components as props. For example fetching data from api when user clicks a button, the local state is updated with the new data.

Unlike props, the data option must be function so that each instance can maintain an independent copy of the returned data object.

data() {
  return {
    users: []
  }
}

We can update the data properties and it will trigger updates in your component via reactivity under the hood. Meaning any template markup referencing that data property or any computed properties derived from that data property will update. You can update these properties dircetly like:

this.message = "Hello";

Contrast this to how local state is updated in React via a function setState():

// React equivalent
this.setState({ message: "Hello" });

In React you can also provide a function to receive previous state as argument when you need to know precise state to make an update.

Vue handles updates asyncronously behind the scenes that batches and handles updates on the next tick. From the Vue docs:

Vue performs DOM updates asynchronously. Whenever a data change is observed, it will open a queue and buffer all the data changes that happen in the same event loop. If the same watcher is triggered multiple times, it will be pushed into the queue only once. This buffered de-duplication is important in avoiding unnecessary calculations and DOM manipulations. Then, in the next event loop “tick”, Vue flushes the queue and performs the actual (already de-duped) work

You can read more about Vue’s async update queue here. In practice, what this means is that you can do the following to update local state and it works out of the box.

// In Vue this will work fine
this.counter += 1;

// In React you will need to pass in function
this.setState((prevState, props) => ({
  counter: prevState.counter + 1
}));

Here is another example of fetching users then setting local state:

<script>
import api from '@/api';
import UserList from '@/components/UserList.vue'

export default {
  components: {
    UserList
  },
  data() {
    return {
      users: []
    }
  },
  created() {
    api.getUsers().then((users) => {
      this.users = users;
    });
  }
}
</script>

<template>
  <div>
    <h3>Number of users: {{ users.length }}</h3>
    <UserList :users='users'/>
  </div>
</template>

Read more about data in the official docs

Computed

Computed properties are functions that return derived values based on other reactive properties, with the added benefit that values are cached based on their dependencies. They are accessed as directly as getter (despite initally being defined as a function).

computed: {
  numberOfUsers() {
    return this.users.length;
  }
}

// referenced later in component (notice there is NO function invokation):
this.numberOfUsers;

Below we expand on the previous users example by converting the this.users.length to a computed property called numberOfUsers:

<script>
import api from '@/api';
import UserList from '@/components/UserList.vue'

export default {
  components: {
    UserList
  },
  data() {
    return {
      users: []
    }
  },
  computed: {
    numberOfUsers() {
      return this.users.length;
    }
  },
  created() {
    api.getUsers().then((users) => {
      this.users = users;
    });
  }
}
</script>

<template>
  <div>
    <h3>Number of users: {{ numberOfUsers }}</h3>
    <UserList :users='users'/>
  </div>
</template>

When you first view that component, initially the the numberOfUsers will be 0 then quickly update to another number after the users have fetched. Therein lies the power of computed properties – they will update for you as other reactive properties are changed.

Compare that to some other alternatives:

// Having a `data` property of `numberOfUsers` (instead of computed) - Not recommended
api.getUsers().then(users => {
  this.users = users;
  this.numberOfUsers = users.length;
});

// Having a `method` and calling it - Not recommended as it will only be called once
<h3>Number of users: {{ numberOfUsers() }}</h3>

methods: {
  numberOfUsers() {
    return this.users.length;
  }
}

That last example, will return the value 0 because initially in component this.users is empty []. When the api receives and updates this.users, it will not call numberOfUsers() a second time because methods are not meant to be reactive in Vue component.

Computed properties are also updated when props are updated, and any mix of props and local state. For example:

props: ['itemName'],
data() {
  return {
    items: []
  }
},
computed: {
  numberOfItems() {
    return `Number of {itemName}s: ${this.items.length}`;
  }
}

If either of itemName or items changes, the numberOfItems is re-evaluated.

Vue’s computed properties is very similar to how other UI frameworks tackle this dependency tracking of properties and DOM updates. For example MobX computed properties and Glimmer’s tracked properties. I really like how Glimmer docs explains this, give it a read!

Methods

Methods are standard functions and automatically scoped to have access to other component properties.

They are often used, but not limited, to:

  • Perform an action based on an event listener on the component
  • Update internal state

Here’s an example with a counter component doing both of these

<!-- Component definition with props -->
<script>
export default {
  data () {
    return {
      counter: 0
    }
  },
  methods: {
    increment() {
      this.counter += 1;
    },
    decrement() {
      this.counter -= 1;
    }
  }
}
</script>

<template>
  <div>
    <span>{{ counter }}</span>
    <button @click='increment'>+</button>
    <button @click='decrement'>-</button>
  </div>
</template>

Note that on the on the event listener we don’t need to invoke the method, just reference it <button @click='increment'>+</button>. You can optionally invoke it and it still works. Usually you want to explicitly invoke if you need to call it with arguments. Here’s an expanded example of such a case:

<!-- Component definition with props -->
<script>
export default {
  data () {
    return {
      counter: 0
    }
  },
  methods: {
    increment(val = 1) {
      this.counter += val;
    },
    decrement(val = 1) {
      this.counter -= val;
    }
  }
}
</script>

<template>
  <div>
    <span>{{ counter }}</span>
    <div>
      <button @click='increment'>+</button>
      <button @click='decrement'>-</button>
    </div>
    <div>
      <button @click='increment(5)'>+5</button>
      <button @click='decrement(5)'>-5</button>
    </div>
  </div>
</template>

While methods can return values, usually this is not standard unless you are evaluating something within a list rendering. This is a common example, and usually can be refactored into component and leverage computed properties.

Let’s revisit a revised user list example from before:

<script>
import UserItem from '@/components/UserItem.vue'

export default {
  components: {
    UserItem
  },
  data() {
    return {
      users: [
        { id: 1, firstName: "Homer", lastName: "Simpson" },
        { id: 2, firstName: "Marge", lastName: "Simpson" }
      ]
    }
  },
  methods: {
    fullName(user) {
      return `${user.firstName} ${user.lastName}`;
    }
  }
}
</script>

<template>
  <div>
    <div v-for='(user, i) in users' :key='i'>
      <h3>Iterating with a method</h3>
      <div>{{ fullName(user) }}</div>
      <h3>Abstracted with a component that can use computed properties</h3>
      <UserItem :user='user'/>
    </div>
  </div>
</template>

Rendering the list of users calls the fullName() method each time for the list, but we can also create a component UserItem that evaluates the fullName as a computed property with caching.

This tweet from Alex Jover Morales puts it nicely:

When you need arguments in a computed prop, it’s time to create a new component. Often it’s the case of the “items” in a v-for directive. You get for free:

✔️ Simpler components

💼 Structured and modularized code

⚡️ Leverage computed prop’s caching

Watchers

Watchers allows us to hook into Vue’s reactive properties by declaring what should be done in response to a change.

Sarah Drasner’s article puts it nicely

Vue grants us some deeper access to into the reactivity system, which we can leverage as hooks to observe anything that’s changing. This can be incredibly useful because, as application developers, most of what we’re responsible for are things that change.

This sounds similar to how Vue rendering updates automatically based on changes to props, data, and computed. So how do watchers differ and how should they be used? Here are some common scenarios:

  • Activating a third party lib (like an animation or charting library) which are not reactive themselves but we want to trigger them when properties change.
  • Re-hydrating API data when incoming props change. Most commonly when you pass in a resource id as prop, and your application router updates and relays that prop.
  • Anything triggered in lifecycle hook (e.g. created or mounted) that you want to trigger more than once in response to property changes

Here’s an example of a user details component that is passed an userId prop:

<script>
import api from '@/api';

export default {
  props: ['userId'],
  data() {
    return {
      user: null
    };
  },
  watch: {
    userId(val) {
      this.fetchUser(val)
    }
  },
  fetchUser(userId) {
    api.getUser(userId).then((user) => {
      this.user = user;
    })
  },
  created() {
    this.fecthUser(userId);
  }
}
</script>

<template>
  <div v-if='user'>{{ user.name }}</div>
</template>

In the above, let’s say our application router changed the url from /users/1 to /users/2. The component above receives a new userId prop of 2, and would need to refetch the user data. Without the watcher, it would still remain on user with id 1.

In general, watchers are less common in applications. If you notice your scenario isn’t any of the above and still have declared a watcher, perhaps there’s opportunity to accomplish the same objective with Vue’s combination of computed/data/props. Ask yourself:

  • Can it be accomplished with computed properties?
  • Am I doing something imperatively that I could do another way?

Summary

  • All of props, data, computed and methods are automatically scoped to the component instance and can be referenced on this.
  • props, data, and computed are all reactive properties. methods are not and must be explicitly called. You can leverage watchers if needed to call methods on an explicit basis.
  • computed cache their return values and are accesed without invoking the function
  • If you need to mutate/update an object, likely will need to use data

Resources