Vue 2 omposition API unwrapping computed within array

We’re using the composition API with Vue 2 due to the Quasar Framework not supporting Vue 3 yet.

Consider this code that stores all data in a reactive object and then returns the data to be read only by using computed:

// src/composables/useApplications
import { computed, reactive, SetupContext } from '@vue/composition-api'

export const useApplications = (root: SetupContext['root']) => {
  const applications = reactive([
    {
      id: 1,
      name: computed(() => root.$t('application.name')),
    },
    {
      id: 2,
      name: 'test',
    },
  ])

  return {
    applications: computed(() => applications),
  }
}

When we would like to use this data to pass it to another component:

<template>
  <q-page padding>
    <div class="q-pa-md row items-start q-gutter-md cursor-pointer">
      <app-application-card
        v-for="card in applicationCards"
        :key="card.id"
        :name="card.name"
      />
    </div>
  </q-page>
</template>
<script lang="ts">
import { defineComponent, reactive } from '@vue/composition-api'
import { useApplications } from 'src/composables/useApplications'

export default defineComponent({
  setup(_, { root }) {
    const { applications: applicationCards } = useApplications(root)

    return { applicationCards }
  },
  components: {
    appApplicationCard: () => import('src/components/ApplicationCard.vue'),
  },
})
</script>

Accessing the data like this generates the following error:

Invalid prop: type check failed for prop “name”. Expected String with value “[object Object]”, got Object

image

When changing the <template> to use the value like shown below the error desappears:

<app-application-card
        v-for="card in applicationCards"
        :key="card.id"
        :name="card.name.value" />

image

The issue is that vscode displays a red line underneath the value property in the template:

Because we use translation the field name needs to be computed. Otherwise Vue will never translate the text correctly.

According to the advice from Vue on GitHub they say:

So I guess using .value in the template isn’t wrong but vscode is wrong underlining it right? Or is there a better way of doing this?

I think you need another layer of reactive(), e.g.:

// src/composables/useApplications
import { computed, reactive, SetupContext } from '@vue/composition-api'

export const useApplications = (root: SetupContext['root']) => {
  const applications = reactive([
    reactive({
      id: 1,
      name: computed(() => root.$t('application.name')),
    }),
    {
      id: 2,
      name: 'test',
    },
  ])

  return {
    applications: computed(() => applications),
  }
}

I didn’t actually realise you could do these nested reactive/computed things at all until I read this post and the linked guide :slight_smile: I had been wanting a way with vuex before to have computed things inside an array but never found a way, so I’m happy!

1 Like

Thanks for your feedback. In the end I settled for using an object instead of an array like this::

// \composables\useApplications.ts
import { computed, reactive, SetupContext, ref } from '@vue/composition-api'

export const useApplications = (root: SetupContext['root']) => {
  const applications = reactive({
    1: {
      name: ref(root.$t('application.name')),
    },
    2: {
      name: ref('test'),
    },
  })

  return {
    applications: computed(() => applications),
  }
}

Then it can simply be used like this:

// src\pages\Applications.vue
<template>
  <q-page padding>
    <div class="q-pa-md row items-start q-gutter-md cursor-pointer">
      <app-application-card
        v-for="(card, id) in applicationCards"
        :key="id"
        :name="card.name"
      />
    </div>
  </q-page>
</template>
<script lang="ts">
import { defineComponent, reactive } from '@vue/composition-api'
import { useApplications } from 'src/composables/useApplications'

export default defineComponent({
  setup(_, { root }) {
    const { applications: applicationCards } = useApplications(root)

    return { applicationCards }
  },
  components: {
    appApplicationCard: () => import('src/components/ApplicationCard.vue'),
  },
})
</script>

Advantages:

  • Reactivity is preserved
  • A unique id is available for each application
  • No errors in vscode

On a side note, it makes no difference in using ref() or computed() for the translated name string. Both work fine. In the end we make it read only by exporting it as a computed property.

Hope this helps others who end up here with similar questions.

1 Like

On another note, for the $t I wrote this helper, which would let you avoid passing the root around like this:

import { getCurrentInstance } from '@vue/composition-api'

// this *should* match the future api of vue-i18n
// See https://github.com/kazupon/vue-i18n/issues/693#issuecomment-630275865
export function useI18n () {
  const vm = getCurrentInstance()
  return {
    t: vm.$t.bind(vm),
    d: vm.$d.bind(vm),
  }
}

Which you can then use like:

const { t } = useI18n()

t('blah')
1 Like

Thanks man! That’s a great tip! Much appreciated :slight_smile:

1 Like

Maybe we should just be using the official Vue I18n composable or simply Vue I18n Next. It seems to do exactly what you made.