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-scopeon something like a<template>.
That's it. I hope this helps simplify your Vue templates!