UPDATE
This was solved using nextTick() as proposed by @Estus Flask.
import { ref, useTemplateRef, nextTick } from 'vue'
...
const handleClick = () => {
color.value = 'blue'
size.value = 50
nextTick(() => {
childRef.value?.render()
})
}
I have Child.vue that draws to canvas. From Parent.vue, I want to be able to change various props of Child.vue, then run its render() function for a redraw.
The problem is when I run handleClick() in Parent.vue, the props for Child.vue have not finished propagating before the render() function is called. This is ascertained by clicking a second time and getting the redraw, and/or by using the hack of adding a delay before calling the render function. Like so:
// Poor person's fix using a delay.
const delay = (ms:number) => new Promise(res => setTimeout(res, ms))
const handleClick = async () => {
color.value = 'blue'
size.value = 50
await delay(10)
childRef.value?.render()
}
In my real app, I have many more props that will change before calling render(). Sometimes only one changes, sometimes five, sometimes two, etc... Never the same ones.
Is there a proper way to solve this race condition?
<!-- Child.vue -->
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
interface Props {
color?: string
size?: number
}
const {
color = 'red',
size = 10,
} = defineProps<Props>()
const canvasRef = useTemplateRef('canvas-ref')
let canvas:HTMLCanvasElement
let ctx:CanvasRenderingContext2D
onMounted(() => {
canvas = canvasRef.value as HTMLCanvasElement
ctx = canvas.getContext("2d") as CanvasRenderingContext2D
render()
})
const render = () => {
ctx.clearRect(0, 0, 100, 100)
ctx.fillStyle = color
ctx.fillRect(0, 0, size, size)
}
defineExpose({ render })
</script>
<template>
<canvas ref="canvas-ref" :width="100" :height="100"></canvas>
</template>
<!-- Parent.vue -->
<script setup lang="ts">
import Child from './Child.vue'
import { ref, useTemplateRef } from 'vue'
const childRef = useTemplateRef('child-ref')
const color = ref()
const size = ref()
const handleClick = () => {
color.value = 'blue'
size.value = 50
childRef.value?.render()
}
</script>
<template>
<Child
:color=color
:size=size
ref="child-ref"
/>
<button @click=handleClick>button</button>
</template>
Send overall changes, not each value individually.
Something like this:
/* App.vue */
<script setup>
import { ref } from 'vue';
import Child from './Child.vue'
const canvasProps = ref()
const handleClick = () => {
canvasProps.value = {
color: 'blue',
size: 50
}
}
</script>
<template>
<button @click="handleClick">handleClick</button>
<Child v-bind="canvasProps"></Child>
</template>
/* Child.vue */
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
interface Props {
color?: string
size?: number
}
const props = withDefaults(defineProps<Props>(), {
color: () => 'red',
size: () => 100,
});
const canvasRef = ref();
const render = () => {
if(!canvasRef.value){
return;
}
const ctx = canvasRef.value.getContext('2d');
ctx.clearRect(0, 0, 100, 100)
ctx.fillStyle = props.color
ctx.fillRect(0, 0, props.size, props.size)
}
onMounted(render);
watch(props, render);
</script>
<template>
<canvas ref="canvasRef" :width="100" :height="100"></canvas>
</template>