Omitted import
statements for the sake of being concise
inputData.ts
export const useInputDataStore = defineStore("inputData", {
state: () => {
return {
currentlyActiveInput: -1
}
},
actions: {
focusInput(inputId: number) {
this.currentlyActiveInput = inputId
}
}
})
InputBox.vue
<script setup lang="ts">
const store = useInputDataStore()
const props = defineProps<{
isActive: boolean,
inputId: number
}>();
const { isActive, inputId } = props;
const onClick = () => {
store.focusInput(inputId);
}
const headerTag = ref<null | HTMLElement>(null);
onMounted(() => {
if (isActive) {
headerTag.value!!.style.visibility = "visible"
} else {
headerTag.value!!.style.visibility = "hidden"
}
});
</script>
<template>
<h1 ref="headerTag">is active </h1>
<h1 @click="onClick">{{ inputId }}</h1>
</template>
ParentComponent.vue
<script setup lang="ts">
const store = useInputDataStore();
const { currentlyActiveInput } = storeToRefs(store);
</script>
<template>
<InputBox
:v-for="index in 10"
:key="index"
:is-active="index === currentlyActiveInput"
:input-id="index"
/>
</template>
Here's what it will look like (I changed the font-size without showing you): Desired Behavior
In the image there is a little gap between the letters. That's the invisible <h1>
tag that I am trying to set to visible when the number is clicked (and then have it be set back to invisible when the next number is clicked).
What's Broken?
When you click on the number, nothing updates. The value in the store is changing (at least, I think). But none of the components get re-rendered. I believe the problem has to do with me using onMounted
Also...
I am aware that there is much simpler way to do this using this code
<h1 v-if="isActive">is active </h1>
I am trying to get a deeper understanding of Vue
and comprehend why my code doesn't work.
1. Destructuring props loses reactivity:
const props = defineProps<{
isActive: boolean,
inputId: number
}>()
const { isActive, inputId } = props // ❌ loses reactivity
The isActive
and inputId
won't change when the parent updates the value.
So you can use the syntax when you know those props won't change for the entire lifecycle of the component, but I'd argue it's bad practice: it could be copied by a fellow developer into a another place where reactivity matters.
To keep reactivity, use props.isActive
inside <script>
and note you can use isActive
in <template>
, without having to declare it as const
- Vue does it for you).
If you really want to destructure, you can keep reactivity by using:
a) toRefs
:
import { toRefs } from 'vue'
const props = defineProps<{
isActive: boolean,
inputId: number
}>()
const { isActive, inputId } = toRefs(props)
b) computed
for single props:
import { computed } from 'vue'
const isActive = computed(() => props.isActive)
However, nobody really does it in the wild, because in both cases the resulting constants are ref()s, which means one still won't be able to use them as isActive
/inputId
inside <script>
but as isActive.value
, which is the same amount of chars as props.isActive
.
The only case where it's actually a gain in chars (excluding the destructuring code) is if you watch
it, as watch
can receive a reactive ref as watched expression; e.g:
watch(isActive, onIsActiveChanged)
So there's no gain, really.
2. v-for
doesn't need binding:
The colon (:
) prefix before a template argument/prop is a shorthand for v-bind:
and is used to interpret the passed value as a JavaScript expression. However, v-for
(along with other built-in vue directives, e.g: v-model
, v-text
, etc...) already expects a JS expression, and does not need the colon prefix.
Note you only need the focused state in the store if it's used in any other place in your app. Otherwise, consider encapsulating the logic into a reusable component (see MyField.vue
in this example).