How to keep reactivity with a composable and a prop in Vue3
You install Vue3 and you decide to use the composable and the composition api
to be faster and follow good practice, but something seems wrong and the destructured
constant from the composable doesn’t update when your prop update? I got you!
Here is a simple example of the problem:
Composable
import { computed } from 'vue';
export const useDropDown = (props) => {
const isSelectedOption = computed(() =>
props.options.map(option => option === props.selectedOption));
return {
isSelectedOption,
};
};
View
<template>
<button
v-for="(option, idx) in options"
:key="idx"
@click="$emit('on-select', option)"
>
<div
:class="{ 'selected-option': isSelectedOption[idx] }"
>
<div v-if="!!option.name" v-html="option.name"/>
<div v-html="option.address" />
</div>
<AppIcon
v-if="isSelectedOption[idx]"
dropdown
icon-name="check"
/>
</button>
</template>
<script setup>
import { useDropDown } from '@/composables/dropdown-modal';
import AppIcon from '@/components/app/AppIcon';
const emit = defineEmits(['on-select']);
const props = defineProps({
options: {
type: Array,
required: true,
},
selectedOption: {
type: Object,
required: true,
},
});
const { isSelectedOption } = useDropDown(props);
</script>
Everything seems good and it should work, right?
We use reactive props and a computed for a composable, so why does the value
of isSelectedOption never change?
Here’s the answer: The lifecycle hook
The setup (composition api) is run before everything.
It takes the place of the beforeCreated/Created since they don’t exist anymore.
So, what happens here is that the code is run on the created and never passes
in the composable anymore because the prop changes don’t re-render the composable.
Hence isSelectedOption keeping its initial value.
To keep this reactivity going, we will need to add something to watch the prop
and update isSelectedOption.
You should NOT use onUpdated in this case because if you do,
you update isSelectedOption in it and it calls an update so it re-renders…
you see where I’m going: Infinite loop.
The solution is to add a watch. There is also another problem.
The destructured element doesn’t go out of the watch. We need a new variable
to manage the value with the reactivity we want. I also add a ref,
just to be sure the reactivity is at its maximum!
<script setup>
import { computed, ref, watch } from 'vue';
import { useDropDown } from '@/composables/dropdown-modal';
import AppIcon from '@/components/app/AppIcon';
const emit = defineEmits(['on-select']);
const props = defineProps({
options: {
type: Array,
required: true,
},
selectedOption: {
type: Object,
required: true,
},
});
let { isSelectedOption } = useDropDown(props);
let selectedOptionArray = ref(isSelectedOption.value);
watch(() => props.selectedOption, (value) => {
({ isSelectedOption } = useDropDown(props));
selectedOptionArray.value = isSelectedOption.value;
});
</script>
Now, we can use selectedOptionArray instead of isSelectedOption
and have reactivity to the infinite and beyond! 🚀