Skip to content

Can I create a local variable in my template?

The code below has some problems. In particular, user.roles.includes('admin') appears three times:

template
<div
  v-for="user in users"
  class="user"
  :class="{ admin: user.roles.includes('admin') }"
>
  <span>{{ user.name }}</span>
  <button :disabled="user.roles.includes('admin')">
    Reset username
  </button>
  <span>Join date: {{ user.joinDate }}</span>
  <button :disabled="user.roles.includes('admin')">
    Ban user
  </button>
</div>
<div
  v-for="user in users"
  class="user"
  :class="{ admin: user.roles.includes('admin') }"
>
  <span>{{ user.name }}</span>
  <button :disabled="user.roles.includes('admin')">
    Reset username
  </button>
  <span>Join date: {{ user.joinDate }}</span>
  <button :disabled="user.roles.includes('admin')">
    Ban user
  </button>
</div>

See it on the SFC Playground.

Ideally there'd be a directive to handle this for us. Maybe something like:

template
<!-- This doesn't exist -->
<div v-const="isAdmin = user.roles.includes('admin')">
<!-- This doesn't exist -->
<div v-const="isAdmin = user.roles.includes('admin')">

Sadly, that isn't currently a thing.

If we just want to reduce the length of the property path, we could use destructuring on the v-for. For example:

template
<div v-for="{ name, joinDate, roles } in users">
<div v-for="{ name, joinDate, roles } in users">

This would then allow us to use just roles instead of user.roles. But it doesn't help with avoiding the repeated calls to includes().

Are repeated calls to includes() actually a problem? In this example, probably not. More generally, there are two problems we might be trying to solve: duplicated logic (DRY) and performance.

computed()

If we weren't inside a loop then we could just use computed():

js
const isAdmin = computed(() => user.roles.includes('admin'))
const isAdmin = computed(() => user.roles.includes('admin'))

But that doesn't work in our example because of the v-for.

An alternative might be to create a helper function:

js
const isAdmin = (user) => user.roles.includes('admin')
const isAdmin = (user) => user.roles.includes('admin')

We could then use isAdmin(user) in our template.

See it on the SFC Playground.

But we still need to call it three times, with the overhead of an includes() call.

Compute the whole array

Rather than trying to use a separate computed() for each user, we could use computed() to reshape our data, so that it's in the form we need.

js
const improvedUsers = computed(() => {
  return users.map((user) => {
    return {
      ...user,
      isAdmin: user.roles.includes('admin')
    }
  })
})
const improvedUsers = computed(() => {
  return users.map((user) => {
    return {
      ...user,
      isAdmin: user.roles.includes('admin')
    }
  })
})

This adds the isAdmin property to each item in our data. We can then iterate over this new array with v-for.

See it on the SFC Playground

Compute a lookup

Rather than reshaping the data to form a single array with all the values, we could instead compute a separate array that just contains the derived values. Something like:

js
const isAdmin = computed(() =>
  users.map(user => user.roles.includes('admin'))
)
const isAdmin = computed(() =>
  users.map(user => user.roles.includes('admin'))
)

This would give us the array [true, false, false]. We can then adjust our v-for to give us the index with v-for="(user, index)", allowing us to access isAdmin[index] in the template.

See it on the SFC Playground

The computed data structure doesn't have to be an array. In some cases it might make more sense for it to be a mapping. For example, we might want to create a mapping structure like this:

json
{
  "Adam": true,
  "Molly": false,
  "Eric": false
}
{
  "Adam": true,
  "Molly": false,
  "Eric": false
}

Here we're assuming that the name field is a unique identifier that we can use to look up the value we want.

We could create a lookup map using something like this:

js
const isAdmin = computed(() => {
  const map = {} // or Object.create(null)

  for (const user of users) {
    map[user.name] = user.roles.includes('admin')
  }

  return map
})
const isAdmin = computed(() => {
  const map = {} // or Object.create(null)

  for (const user of users) {
    map[user.name] = user.roles.includes('admin')
  }

  return map
})

We'd then use isAdmin[user.name] in the template.

See it on the SFC Playground

The code above uses a for/of loop to create the mapping object, but there are various other ways you could write it. For example, with map() and Object.fromEntries():

js
const isAdmin = computed(() => Object.fromEntries(users.map(user => [
  user.name,
  user.roles.includes('admin')
])))
const isAdmin = computed(() => Object.fromEntries(users.map(user => [
  user.name,
  user.roles.includes('admin')
])))

Introduce a component

Whenever you find yourself using v-for around some non-trivial content, it's worth considering whether you should extract a separate component for that repeated section. You can think of repeating content with v-for as a form of reuse, and that makes components a natural fit.

In the case of our example, a new component can focus on just rendering a single user, without needing to worry about the loop. There are a couple of ways we might approach this in practice, making isAdmin either a computed() or a prop.

If you're specifically interested in performance, then introducing a component is a trade off. It will make the initial render slower, as there is an overhead incurred to create each component instance. But the extra components help to split up rendering into smaller pieces, so they can potentially improve the performance of rendering updates. See https://vuejs.org/guide/best-practices/performance.html#update-optimizations for more details on squeezing performance out of component updates.

Child computed()

The first approach is to pass the user object as a prop, and then use computed() to derive the value we need:

js
import { computed } from 'vue'

const props = defineProps({
  user: Object
})

const isAdmin = computed(() => props.user.roles.includes('admin'))
import { computed } from 'vue'

const props = defineProps({
  user: Object
})

const isAdmin = computed(() => props.user.roles.includes('admin'))

See it on the SFC Playground.

Child prop

The other way we could approach it is to introduce a prop for isAdmin. While we could pass user and isAdmin as props, it might be cleaner in this example to have props for isAdmin, joinDate and name instead:

js
defineProps({
  isAdmin: Boolean,
  joinDate: String,
  name: String
})
defineProps({
  isAdmin: Boolean,
  joinDate: String,
  name: String
})

See it on the SFC Playground.

The value of isAdmin is calculated in the parent template. That calculation will be repeated each time the parent component re-renders. However, it only happens once per iteration (rather than 3 times), and the children won't be re-rendered unless one of the props changes.

The v-for hack

Even without a dedicated directive, there are already a couple of ways to introduce a local variable within a Vue template.

One way to do it is with v-for. We can exploit that to introduce the local variable we need:

template
<div v-for="isAdmin in [user.roles.includes('admin')]">
<div v-for="isAdmin in [user.roles.includes('admin')]">

We're creating a temporary array with a single item, then iterating over that array to get the value we need for isAdmin.

See it on the SFC Playground.

Using a scoped slot

The other way to introduce a local variable inside a Vue template is to use a scoped slot.

To use this approach, we need to introduce a separate component. There are various ways to write that component. For example, we might use a functional component, like this:

js
function GetValue(props, { slots }) {
  return slots?.default(props.value)
}
function GetValue(props, { slots }) {
  return slots?.default(props.value)
}

We can then use it something like:

template
<GetValue :value="user.roles.includes('admin')" v-slot="isAdmin">
  <!-- isAdmin is now available here -->
</GetValue>
<GetValue :value="user.roles.includes('admin')" v-slot="isAdmin">
  <!-- isAdmin is now available here -->
</GetValue>

See it on the SFC Playground.

Alternatively, we could use the attribute name as the variable name, allowing for multiple variables to be introduced at the same time:

js
function GetValue(props, { slots }) {
  return slots?.default(props)
}
function GetValue(props, { slots }) {
  return slots?.default(props)
}

With:

template
<GetValue :isAdmin="user.roles.includes('admin')" v-slot="{ isAdmin }">
  <!-- isAdmin is now available here -->
</GetValue>
<GetValue :isAdmin="user.roles.includes('admin')" v-slot="{ isAdmin }">
  <!-- isAdmin is now available here -->
</GetValue>

See it on the SFC Playground