The Problem
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>
Common Solutions
We could describe this problem as "computed properties with arguments", and it already has some established solutions out there:
Outsource Components
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.
Memoize Method Results
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.
My Approach
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:
Caveats
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!
Top comments (20)
There is a simpler way to achieve this, which is probably even less known, without the use of an extra component and it goes like this:
For those that may now be thinking, "Hey, I didn't know there was an undocumented
:set
in Vue", there isn't. What I'm doing here is relying on the fact that Vue will evaluate the JavaScript of any bound attributes and I just chose to invent an attribute called:set
.As a reminder, a bound attribute is one that is prefixed with a
:
orv-bind:
. The JavaScript expression inside the double quotes will be evaluated in the context of the current component and theitem
variable will still be available outside of thev-for
in which it is being set. So, it's not a local variable, as such, rather, it's a locally assigned component scope (non-reactive) variable.Do note that this attribute does not have to be declared in your data component first. However, if you don't declare it, it will not be reactive. In the example above it does not matter to me if it's reactive, but it's something to keep in mind if you use this pattern
Here is a fork of Florian's code (thank's Florian) showing the pattern at work.
codesandbox.io/s/6nwyw3zzwz
This may work in JS but TypeScript can't make heads or tails of this. The referenced function will be marked as un-used, causing a TS error, and the unknown variable will also be marked as unknown. You can define the var in the
<script>
block, but it will still leave the previous error.A better approach may be to use a WeakMap in the getUserData function. Like:
Repeated calls to
getUserData
within the loop, then, will instantly return the item, and once we're out of that iteration of the loop, the cached result will be garbage collected.I didn't put in TypeScript annotations but TS handles this approach much better as well.
This is definitely a clever hack. A little bit dirty, but absolutely clever. 😁
Thanks. I would like to add, though, this is neither dirty nor a hack.
Vue does allow us to create local variables on
this
and just cautions that they won't be reactive (I read it somewhere in the docs).As for setting values, well, it's just like an event handler, where one would do exactly the same:, for example:
@click="item=$event"
I know it feels like a hack, at first looks, because it's too simple, but it's legit.
Perhaps, I cheated a bit by using an attribute named
:set
;)I think my perception of this as a "hack" mostly refers to "using assignment expressions as attributes". It just doesn't feel right (to me, personally). Sure, it's a thing we've done forever in
on*
event attributes, but at least the semantics of events do match this better than the semantics of vanilla attributes.That's pretty much the same as
let foo; while (foo = expression) { /* use foo */ }
— it's a thing that is frowned upon by many readers and linters, but it's used because the clean way would be annoyingly verbose.I hear you.
How does this solve the fact that the call is not memoized?
It doesn’t. It just helps to avoid using deeply nested object references or making calls to the same function several times. You could use a computed, without a parameter, if you want caching.
Hmmm...no you can't use a computed here. It's in a loop. That was the whole point here.
Indeed, you’re right. A normal computed wouldn’t have access to the v-for parameter here and a parametrized computed wouldn’t be cached.
If you need caching then you will need a different technique.
You could declare a data variable and then assign the v-for iterator to it, like I have shown in my example. In this way, a computed would have access to that data variable with the iterator’s value. Would this work for you?
My way would be to create computed property with stored results of
getUserData
function execution:After that is can simply be used like:
More Vue-ish as for me.
Yes, this is the obvious one and should always be preferred where applicable.
Template variables are a better (as in "more ergonomical") fit when you don't need to process all the array items and don't want to duplicate logic.
I'm a newbie when talking about Vue - well, programming in general, but your solution makes sense. I'll try to make use of it in an Angular based app I'm working on at office, where I've encountered the need of keeping some computed values close. Thanks for sharing!
Glad it helped. The approach was heavily inspired by React's render props. I have no clue though whether it's applicable to the Angular world.
I guess we'll find out 😁
How come the user data isn't "gotten" up front? I mean, creating calls for data in a loop (even in a program) is a bad pattern to begin with, isn't it?
Scott
The user data thing is a completely made up example, I didn't come up with something better that's pragmatic and also immediately "clicks" with the reader.
But yeah, if you mean network requests with "calls for data", that would not necessarily be a bad idea in a loop, but they certainly do not belong inside a template. In a real world example, the kind of data to store usually is some calculation or data structure creation you don't need in each iteration of the loop.
You could certainly create all needed data upfront and move the logic to other places (e.g. by memoizing or refactoring to additional components, as mentioned in the post) but to me it actually happens quite a lot that such an approach might be "cleaner" but is not worth sacrificing the amount of clarity and ease of use.
Yup. It's a bad example. Glad you agree. User data for the front end should not be in different places. In other words, there is no reason to have an array of users to then have to get the data to fill that array in a loop. That gathering of data should be done before the data reaches the loop.
If you pass anything to child components being looped over in a v-for, that passed data should be relatively small bits of data already "gotten". Or it can be data that can be broken down through the hierarchy of components. Most definitely each smaller component should NOT be calling for more data. That's sidestepping one-way data flow.
So, I'll be so bold to say, you probably couldn't find a practical use case, because there isn't one, because trying to call for data from passed in props or data is simply a poor pattern. If you are finding you need to do that, you are somehow starting off with an incorrect mental model of SFCs, component hierarchy and proper reactive data flow.
Scott
This can be useful to prevent v-for and v-if in one element.