vuejs3async-components

Calling a method of an async component from the parent component onMounted (Vue3)


I have this general component which can render different sorts of components asynchroneously:

<script setup>
import { defineAsyncComponent, defineProps, defineEmits, defineExpose, onMounted, ref, computed, toRefs, nextTick, watch } from 'vue';

const props = defineProps({
    component: { type: String },
});

const { component } = toRefs(props);

const components = {
  componentOne: defineAsyncComponent(() => import('...'),
  componentTwo: defineAsyncComponent(() => import('...'),
  // and so on
}

const currentComponent = computed(() => {
  return component[props.component] || null
})

const currentComponentRef = ref(null);

const methodA = ref(() => null)

onMounted(async () => {
  if (currentComponentRef.value && currentComponentRef.value.methodA) {
    methodA.value = currentComponentRef.value.methodA
  }
})

watch(currentComponentRef, async (newVal) => {
 if (newVal) {
  await nextTick()
  if (newVal) {
    methodA.value = newVal.methodA
  }
 }
})

defineExpose({
  methodA
})

</script>

<template>

<component
  :is="currentComponent"
  ref="currentComponentRef"
/>

</template>

Now when using this component from a parent component, calling its methodA onMounted gets on the call stack before the component can define methodA itself. I want to avoid using timeouts in the parent.

Is there another way to make sure exposed methods are ready to be called onMounted ?

The only way I found on the parent component to wait is the following:

const methodAFromComponent = computed(() => {
  if(component.value && component.value.methodA) {
    return component.value.methodA
  }
})

watch(methodAFromComponent, (newVal) => {
 if (newVal) {
   console.log(newVal())
 }
})

But if there is a way to avoid users of the component to have to do that, it would be ideal.


Solution

  • You can postpone calls in a reactive array and when your async component is ready (watch it) - execute the calls:

    See on Vue SFC Playground

    <script setup>
    import { defineExpose, ref, watch, defineAsyncComponent, shallowReactive} from 'vue';
    
    const props = defineProps({component: String});
    
    const components = {
      componentOne: defineAsyncComponent(async () => ({setup:  (_, {expose}) => (expose({methodA: () => alert('componentOne')}), () => 'componentOne')})),
      componentTwo: defineAsyncComponent(async () => ({setup:  (_, {expose}) => (expose({methodA: () => alert('componentTwo')}), () => 'componentTwo')}))
    };
    
    const currentComponentRef = ref(null);
    const methodACalls = shallowReactive([]);
    
    watch([methodACalls, currentComponentRef], () => {
      if(!currentComponentRef.value) return;
      while(methodACalls.length) currentComponentRef.value.methodA(...methodACalls.shift());
    })
    
    defineExpose({
      methodA: (...args) => methodACalls.push(args)
    });
    
    </script>
    
    <template>
    
    <component
      :is="components[component]"
      ref="currentComponentRef"
    />
    
    </template>
    

    Usage:

    <script setup>
    import {ref, onMounted} from 'vue';
    import Parent from './Parent.vue';
    
    const component = ref('componentOne');
    const $parent = ref();
    
    onMounted(() => {
      $parent.value.methodA();
    });
    
    </script>
    <template>
      <parent ref="$parent" :component="component"/>
    </template>