A Pen by Angel Blanco on CodePen.
Created
April 16, 2021 13:42
-
-
Save MasterGroosha/627cc84960814b7396f45862868c3c26 to your computer and use it in GitHub Desktop.
Vue rendering: preventing unnecesary re-rendering by passing objects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Title</title> | |
</script> | |
</head> | |
<body> | |
<div id="app"> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const grandChildB = { | |
name: 'GrandChildB', | |
props: ['value'], | |
template: ` | |
<div class="component-tree-b"> | |
<p> | |
This is grand child B. This one is the only one that uses | |
the message. Because of passing values inside objects, | |
it is the only one with a dependency to the 'messageB' | |
field value and will be the only one to get re-rendered. | |
We emit the event 'update:messageB' to parents so that the | |
values get updated at the top level | |
</p> | |
<p> | |
Edit the message here: <input type="text" | |
:value="value.messageB" | |
@input="$emit('input',$event.target.value)"/> | |
</p> | |
<p> | |
You'll notice every key press only causes this component to | |
be re-rendered. | |
</p> | |
<p> | |
The message is this: {{ value.messageB }} | |
</p> | |
</div>` | |
} | |
const childB = { | |
name: 'ChildB', | |
props: ['value'], | |
components: { | |
grandChildB | |
}, | |
template: ` | |
<div class="component-tree-b"> | |
<p> | |
This is child B. This one doesn't use the message either and | |
also just passes the whole settings object down to the | |
grandchild who is the one that will emit the events to | |
modify | |
it, and it will pass up events of 'update:messageB' to the | |
parent so it gets updated. As it has no reference to | |
'messageB' either, it will not get re-rendered. | |
</p> | |
<grand-child-b :value="value" | |
@input="$emit('update:messageB', $event)"></grand-child-b> | |
</div>` | |
} | |
const componentB = { | |
name: 'ComponentB', | |
components: { | |
childB | |
}, | |
data() { | |
return { | |
settings: { | |
messageB: '' | |
} | |
}; | |
}, | |
template: ` | |
<div id="component-b" class="component-tree-b"> | |
<h1>The RIGHT way to do it</h1> | |
<p> | |
This is component B. We pass the message encapsulated inside | |
a 'settings' object instead of directly to children to | |
avoid having a reference to 'messageB' in this template. | |
And we listen to events from the children to update | |
it's value. We can't use v-model because v-model would | |
update the settings object, not the specific field that our | |
children want to update only. We follow Vue's convention | |
of using 'update:fieldNameHere' events when updating a | |
sub-field of the passed in prop instead of input (input | |
is normally used to update the entire value of the prop | |
that was passed in with v-model). | |
</p> | |
<p> | |
As a result of not having any references to 'messageB' in | |
this component, it doesn't get re-rendered any time the | |
children emit events to update the value of 'messageB' | |
</p> | |
<child-b :value="settings" | |
@update:messageB="settings.messageB = $event"></child-b> | |
</div> | |
` | |
} | |
const grandChildA = { | |
name: 'GrandChildA', | |
props: ['value'], | |
template: ` | |
<div class="component-tree-a"> | |
<p> | |
This is grand child A. This one is the only one that uses | |
the message so it makes sense for it to get re-rendered when | |
the messageA changes, but not the others | |
</p> | |
<p> | |
Edit the message here: <input type="text" :value="value" | |
@input="$emit('input',$event.target.value)"/> | |
</p> | |
<p> | |
You'll notice every key press causes all three components to | |
re-render. A hook is connected to the updated | |
event of the root Vue instance to catch and log any time a | |
component re-renders. | |
</p> | |
<p> | |
The message is this: {{ value }} | |
</p> | |
</div>` | |
} | |
const childA = { | |
name: 'ChildA', | |
props: ['value'], | |
components: { | |
grandChildA | |
}, | |
template: ` | |
<div class="component-tree-a"> | |
<p> | |
This is child A. This one doesn't use the message either but | |
will also get re-rendered just for passing the prop down to | |
the | |
children | |
</p> | |
<grand-child-a :value="value" | |
@input="$emit('input', $event)"></grand-child-a> | |
</div>` | |
} | |
const componentA = { | |
name: 'ComponentA', | |
components: { | |
childA | |
}, | |
data() { | |
return { | |
messageA: '' | |
}; | |
}, | |
template: ` | |
<div id="component-a" class="component-tree-a"> | |
<h1>The WRONG way to do it</h1> | |
<p> | |
This is component A. Due to passing direct fields as props | |
to children, you'll notice this component gets re-rendered | |
every time the 'messageA' changes due to child events, even | |
though this component does not use 'messageA' | |
</p> | |
<child-a v-model="messageA"></child-a> | |
</div> | |
` | |
} | |
const app = { | |
name: 'App', | |
components: { | |
componentA, | |
componentB | |
}, | |
created() { | |
Vue.mixin({ | |
updated() { | |
this.$root.$emit('on-re-rendering', this.$options.name) | |
}, | |
}) | |
this.$root.$on('on-re-rendering', (component) => { | |
this.logs.unshift({ | |
componentName: component, | |
timestamp: new Date().toISOString() | |
}) | |
}) | |
}, | |
template: ` | |
<div> | |
<div> | |
<component-a></component-a> | |
<component-b></component-b> | |
</div> | |
<div id="logs-container"> | |
<input type="button" @click="logs = []" value="Clear logs"/> | |
<ul> | |
<li v-for="(log) in logs" :key="log.timestamp"> | |
{{ log.timestamp }}: Re-rendering component {{ log.componentName }} | |
</li> | |
</ul> | |
</div> | |
</div> | |
`, | |
data() { | |
return { | |
logs: [] | |
}; | |
}, | |
}; | |
new Vue({ | |
render: h => h(app) | |
}).$mount('#app'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html, body { | |
margin: 0; | |
padding: 0; | |
} | |
.component-tree-a, .component-tree-b { | |
border: 1px solid gray; | |
padding: 10px; | |
} | |
#component-a, #component-b { | |
display: inline-block; | |
margin: 0 5px; | |
font-family: sans-serif; | |
font-size: 16px; | |
line-height: 22px; | |
vertical-align: top; | |
} | |
#component-a { | |
width: 30%; | |
} | |
#component-b { | |
width: 60%; | |
} | |
h1 { | |
font-size: 40px; | |
line-height: 40px; | |
} | |
.component-tree-a { | |
border-color: indianred; | |
} | |
.component-tree-b { | |
border-color: cornflowerblue; | |
} | |
#logs-container { | |
padding: 20px 0 0 20px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment