Vue reactivity and VueUse computedAsync mistery

59 Views Asked by At

Would anyone have a hint as to why localData updates properly, while storeData remains stuck at -1 in this Vue SFC playground??

Reactivity issue with computedAsync

The entire code is below; the real use case involves REST calls which are mocked here using await timeout(3000);

If I use localData and storeData directly in the template both update fine. If I go through the computeds however only the local data updates. Interestingly enough if I use both storeData and computedStoreData in the template they both update fine.

<template>
  <h2>Test data from local: {{ computedLocalData }}</h2>
  <h2>Test data from store: {{ computedStoreData }}</h2>
</template>

<script lang="ts" setup>
import {computedAsync} from "@vueuse/core";
import {computed,  reactive, ref, type Ref, unref} from "vue";

const storeData = getTestData(3);
const computedStoreData = computed(() => storeData.value)

const localData = ref(-1);
const computedLocalData = computed(() => localData.value)

setTimeout(() => localData.value = 3, 2000)

// Store

const _testDatas = reactive<Record<number,  Ref<number>>>({});

function timeout(ms : number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function getTestData(venueId : number) {
  return computed(() => {
    if (! (venueId in _testDatas)) {
      _testDatas[venueId] = computedAsync(async () => {
        await timeout(3000);
        return venueId
      }, -1, {lazy: false});
    }
    return unref(_testDatas[venueId]);
  })
}

</script>

Edit: Even more interesting, adding this line makes it work:

watch(storeData,  () => {})

[Edit 2]: Maybe I should explain why I ended up with trying to dynamically create computed composables. I have a dynamic set of venues that are obtained from the server:

venues = await fetchVenues()

Each venue then has a set of services:

venueServices = await fetchServices(venueId)

My components need the services for a venue, where the venue can be either static, or picked by the user, or supplied as the prop of a custom web component.

Obviously I want any given fetchServices call to happen at most once throughout the SPA for a given venue, and only if needed. I have tried several patterns but I can't come up with a robust working solution.

1

There are 1 best solutions below

0
Franck On

I found a slightly different approach that seems to work (pseudo-code since I edited it to simplify):

type VenueArg = MaybeRef<number | HasVenueId | null | undefined>;
type ServicesById = Record<number, ServiceViewSlots>;
type ServicesByVenue = Record<number, Ref<ServicesById>>

const _servicesByVenue: ServicesByVenue = {};
 
export function venueServices(venueArg?: VenueArg) {
    return computed(() => {
        const venueId = _unwrapVenueArg(venueArg);
        if (venueId == null) return {} as ServicesById;
        if (!(venueId in _servicesByVenue)) {
            _servicesByVenue[venueId] = ref({})
            _fetchServices(venueId);
        }
        return _servicesByVenue[venueId].value;
    })
}

function _fetchServices(venueId: number) {
    epClient.getServices<ServiceViewSlots[]>(venueId)
        .then(r => {
            _servicesByVenue[venueId].value = r.reduce<Record<number, ServiceViewSlots>>((p, n) => {
                p[n.id] = n
                return p
            }, {} as Record<number, ServiceViewSlots>);
        })
        .catch(e => console.error("Fetch failed: " + e));
}