How to Make Better Vue Component Props?

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:
- Vue2: repository / demo
- Vue3: repository / demo
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.

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.

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

Senior front-end developer with strong math background from Vilnius, Lithuania.
Passionate about vue.js, reactivity, browser performance.