vuejs3

How to dynamically update props of components created with h()?


I have a very simple progress bar component:

<script setup lang="ts">
defineProps<{ progress: number }>();
</script>
<template>
    <div class="progress" role="progressbar" :aria-valuenow="progress" aria-valuemin="0" aria-valuemax="100">
        <div class="progress-bar" :style="{ 'width': progress + '%' }"></div>
    </div>
</template>

I created these components dynamically using h because I need to generate them and I intertwine them with other components in a single big elements.

I have something like that:

import ResultProgressLine from './ResultProgressLine.vue';
import { h, ref, type VNode } from 'vue';

const contentRows = ref<VNode[]>([]);
const contentProgress: Map<number, VNode> = new Map();

// create the component
contentProgress.set(id, h(
    ResultProgressLine, {
        "progress": 0
    })
);
contentRows.value = contentRows.value.concat(contentProgress.get(id));

I then mount it like so:

<component v-for="row, index in contentRows" :key="index" :is="row"></component>

How can I then dynamically update the progress props of the ResultProgressLine dynamically?

Note: The <component> binds heterogeneous components, so I do not think I can use pass the ref there.

Demo on play.vuejs.org.


What I tried so far:

const progress = ref(0);
h(ResultProgressLine, { progress: progress });
// this almost works but does not re-render the component, so it only updates
// on the next "global" render
contentProgress.get(id)!.props.progress = 50;

Solution

  • I think you selected unconventional path to implement what you want. First your vnode should reactive which is not and using a reactive vnode is something unusual. Usually a reactive data is converted to vnodes and provided to Vue to check against existing vnodes and render. So in your case a row/line should be reactive. For contentRows I would recommend a shallow reactive array. I call it a shallow reactive array / reactive item design pattern:

    Playground

    <script setup>
    
    import { h, reactive, shallowReactive } from 'vue';
    import Comp from './Comp.vue';
    
    const contentRows = shallowReactive([]);
    
    const doClick = () => {
        const row = reactive({progress: 0});
        setTimeout(() => {
          row.progress = 50;
        }, 1000)
    
        contentRows.push(row);
    };
    
    </script>
    
    <template>
      <button @click.prevent="doClick">Click</button>
      <div class="content" ref="contentDiv" id="content-div">
          <component :is="() => contentRows.map(row => h(Comp, row))" />
      </div>
    </template>