This has first been published as an article on dev.to.
From time to time, I have the need to temporarily store the results of a method call in Vue templates. This is particularly common inside loops, where we cannot easily use computed properties.
Basically what we'd want to avoid is this:
<!-- List.vue -->
<ul>
<li v-for="id in users" :key="id">
<img :src="getUserData(id).avatar"><br>
🏷️ {{ getUserData(id).name }}<br>
🔗 {{ getUserData(id).homepage }}
</li>
</ul>
We could describe this problem as "computed properties with arguments", and it already has some established solutions out there:
The pretty much canonical way is done through refactoring: We could outsource the <li>
items into their own <ListItem>
component.
That component would receive the id
as a prop and store the according metadata in a computed property which is then cached by Vue until it needs to be re-evaluated.
<!-- List.vue -->
<ul>
<ListItem v-for="id in users" :key="id" :id="id" />
</ul>
<!-- ListItem.vue -->
<li>
<img :src="metadata.avatar"><br>
🏷️ {{ metadata.name }}<br>
🔗 {{ metadata.homepage }}
</li>
However, this approach can be pretty tedious to write and maintain: all pieces of data we need inside each list item have to be passed down to the <ListItem>
as props.
It can also be hard to follow as a reader — particularly if the <ListItem>
component is very small. Then it may easily contain four lines of template code followed by 25 lines of props definition boilerplate.
We could also memoize the results of getUserData()
.
However this can be tedious to implement as well, it usually only works with serializable input data — and of all approaches, adding another layer of memoization on top of Vue feels like suiting the Vue way™ the least.
For my projects, I like to use another (less obvious, and AFAICT less common) approach: I create a helper component I call <Pass>
.
It's really, really tiny:
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
Basically this is a placeholder component which does not render a DOM element itself but passes down all props it receives to its child.
So, let's rewrite our list with the <Pass>
helper:
<!-- List.vue -->
<ul>
<Pass v-for="id in users" :key="id" :metadata="getUserData(id)">
<li slot-scope="{ metadata }">
<img :src="metadata.avatar"><br>
🏷️ {{ metadata.name }}<br>
🔗 {{ metadata.homepage }}
</li>
</Pass>
</ul>
This will only evaluate getUserData()
once: when <Pass>
is rendered. Nice and clean, isn't it?
Also, here's a CodeSandbox where you can fiddle around with the example I described.
To be fully honest, there are a few drawbacks to this approach:
- The helper component utilizes a scoped slot for passing data. This means,
<Pass>
can only have exactly one child component. - Another limitation to this approach is that the markup injected into the slot must render a real DOM node. We can't just set the
slot-scope
on something like a<template>
.
That's it. I hope this helps simplify your Vue templates!