4

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): example of it 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.

2 Answers 2

2

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).

Sign up to request clarification or add additional context in comments.

Comments

1

Besides you have already aware the correct way:

<h1 v-if="isActive">is active </h1>

There are 2 issues in your code:

  1. The correct syntax for a range of v-for should be without the colon before v-for.

    :v-for="index in 10"

should be changed to:

v-for="index in 10"
  1. The onMounted lifecycle hook in Vue is only called once, if you want to update the visibility style of the headerTag whenever the isActive prop changes, you should use a watcher instead of the onMounted hook.

InputBox.vue

<script setup lang="ts">
import { useInputDataStore } from '@/store/inputData.ts';
import { ref, toRef, watch } from 'vue';

const store = useInputDataStore();

const props = defineProps<{
  isActive: boolean;
  inputId: number;
}>();

const { inputId } = props;
const isActive = toRef(props, 'isActive');

const onClick = () => {
  store.focusInput(inputId);
};

const headerTag = ref<null | HTMLElement>(null);
watch(
  isActive,
  newVal => {
    if (!headerTag.value) return;
    if (newVal) {
      headerTag.value.style.display = 'block';
    } else {
      headerTag.value.style.display = 'none';
    }
  },
  { immediate: true }
);
</script>
<template>
  <h1 ref="headerTag" style="display: none">is active</h1>
  <h1 @click="onClick">{{ inputId }}</h1>
</template>

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.