How to Make Better Vue Component Props?

Multiple project dashboard template

Component is the core concept of the entire vue.js ecosystem. A better understanding of how the props are passed down and when the component rendering happens helps build a performing system. In this article, we explain how to pass vue js component props correctly to avoid unnecessary re-rendering.

There’re 2 repositories created for this article, one is for vue2, another one is for vue3:

Let’s take as an example the list of items and allow every item to be checked by the user. This is like a batch action, a user can check multiple items and delete them at once. Also, allow item sorting by drag&drop. Let’s follow the normalizr approach, store item ordering separately as well as check item ids:

export interface IState {
  ids: string[]
  itemsByIds: { [key: string]: { id: string, title: string } }
  checkedIds: string[]
}

The following getter will provide extended items with boolean isChecked property:

export const getters = {
  extendedItems (state) {
    return state.ids.map(id => ({
      ...state.itemsByIds[id],
      isChecked: state.checkedIds.includes(id)
    }))
  }
}

Later we’ll show that this getter is not good because it extends/creates new objects instead of reusing existing ones. But first, let’s use it in a very wrong way:

// App.vue:
<ItemWithRenameById for="id in $store.state.ids" :key="id" :item-id="id" />
// ItemWithRenameById.vue:
<template>
  <div v-if="item">
    <input :checked="item.isChecked" type="checkbox">
    <input :value="item.title">
  </div>
</template>
<script>
export default {
  props: ['itemId'],
  computed: {
    item () { return this.$store.getters['extendedItems'][this.itemId] }
  },
  updated () {
    console.log('ItemWithRenameById updated')
  }
}

This code works, here’s the demo for vue2 and vue3. But if we try to rename, check or reorder any item, we see, that the updated hook runs not only for the target item but for every item in the list. The hook triggers when the component detects changes. It runs the render function, builds the new virtual dom, and compares it with the current version. These are fast and lightweight operations, but if there’re 1000 items, running render function 1000 times when only one item is updated sounds terrible.

vue js component props

Let’s figure out why this happens. Every vue component has a render function that provides virtual DOM. Vue builds a dependency tree to detect when the render function should be reevaluated. The tree is the list of reactive variables used during the render (it’s determined on the dry-run stage).

ItemWithRenameById component uses item computed in the template which uses extendedItems getter and itemId prop. Hence, the render function will run when either itemId or extendedItems is changed.

When any item is renamed, extendedItems getter builds a new array consisting of new extended item objects. All the objects except one are internally the same, but vue does not use expensive recursive object comparison, new object means change and triggers the render function.

Stage 1: Item rename fix

Let’s try to refer to the store directly instead of using the getter. Here’s the updated ItemWithRenameById.vue:

<template>
  <div v-if="item">
    <input :checked="isChecked" type="checkbox">
    <input :value="item.title">
  </div>
</template>
<script>
export default {
  props: ['itemId'],
  computed: {
    item () { return this.$store.state.itemsByIds[this.itemId] },
    isChecked () { return this.$store.state.checkedIds.includes(this.itemId)
  },
  updated () {
    console.log('ItemWithRenameById updated')
  }
}

Now item rename works correctly (vue2, vue3). This happens because when an item is renamed only one element of itemsByIds changes. But isChecked prop still refers to the entire checkedIds array. That’s why if we check any item, all the items in the list will be re-rendered.

vue js component props

Stage 2: vue3 + scope variables in event handler

Item ordering works correctly in vue2, but in vue3 every drag&drop causes all the items to re-render. This happens because vue3 does not cache an event handler if it refers to a scope variable:

// Item is not cached because event handler refers to a scope variable;
// Every update will create a new event function which causes the component to update:
<div v-for="item in items" :key="item.id">
  <Item @event="eventHandler(item.id)" />
</div>
// Item is cached because there's no reference to a scope variable:
<div v-for="item in items" :key="item.id">
  <Item @event="eventHandler" />
</div>

Here’s an updated version for vue3 with correctly working drag&drop.

An alternative way to fix re-rendering is to declare events by adding emits option to the item component. Here’s the working version which uses emits.

Stage 3: item check/uncheck fix

Our general mistake is that the item component internally refers to store variables that keep the data for many items. Render function references should be as granular as possible. That’s why if we want to know if the item is checked or not, instead of using state.checkedIds from inside the component we should send just a boolean isChecked prop.

Let’s prepare a better getter for it that will extend the existing objects instead of creating new ones:

export const getters = {
  extendedItems (state) {
    return state.ids.map(id => ({
      id,
      data: state.itemsByIds[id], 
      isChecked: state.checkedIds.includes(id)
    }))
  }
}

And finally here’s the updated and working version for vue2 and vue3:

// App.vue:
<ItemWithRename 
  for="item in $store.getters.extendedItems" 
  :key="item.id"
  :item="item.data"
  :is-checked="item.isChecked"
/>
// ItemWithRename.vue:
<template>
  <div v-if="item">
    <input :checked="isChecked" type="checkbox">
    <input :value="item.title">
  </div>
</template>
<script>
export default {
  props: ['item', 'isChecked'],
  updated () {
    console.log('ItemWithRenameById updated')
  }
}

Liked an article? Share it with your friends.

Teamhood uses cookies, to personalize content, ads and analyze traffic. By continuing to browse or pressing "Accept" you agree to our Cookie Policy.