New Sep 18, 2024

Tracking down the root cause of unexpected reactivity in Vue 3 components

Libraries, Frameworks, etc. All from Newest questions tagged vue.js - Stack Overflow View Tracking down the root cause of unexpected reactivity in Vue 3 components on stackoverflow.com

I am currently working on a fairly simple Vue-3 app, but experiencing an issue I'm not sure how to find the root cause of. In my app I have a view which is being rendered via the router. This view has some simple state (an object ref for storing the values of some text fields, and a boolean ref for toggling a modal on and off), and also makes use of a single state object from a pinia store.

The very first time I load the view I can edit the text fields which are bound to the object ref with ease, but the very first time I attempt to open the modal by clicking a button two things happen: The text fields are cleared, and the modal fails to open. After the button has been clicked the very first time, everything works the way I expect, and I can open and close the modal without disrupting the state of the view.

I have attempted adding watchers to all of the internal state refs so I can see if the values are changing, but I am not seeing those watchers triggered when the text fields are cleared after the first button click, and I'm at a loss for figuring out what might be causing this reactivity to occur.

EDIT: After a bit of digging I can see that the very first time I click the button to show the modal onMounted() fires on the view itself. Could this somehow be related to the modal being teleported into the body tag and causing the whole view to be remounted? /EDIT

For context, here is the code of the view (TemplateEditorView.vue):

<script setup>

import TemplateSelectorComponent from '../components/TemplateSelectorComponent.vue' import useTemplate from '../composables/useTemplate' import { computed, ref, watch } from 'vue' import AddSectionModal from '../components/AddSectionModal.vue' import { useTemplateStore } from '../stores/template' import { storeToRefs } from 'pinia'

const templateStore = useTemplateStore() const { templates } = storeToRefs(templateStore) const { selectedTemplate, selectedTemplateKey, updateSelectedTemplateKey } = useTemplate()

const newTemplate = ref({ id: '', name: '', templateText: '', sections: [] }) const showModal = ref(false)

const handleModalSubmit = function (section) { newTemplate.value.sections.push(section) showModal.value = false }

const handleSave = function () { if (saveEnabled.value) { templates.value[newTemplate.value.id] = newTemplate.value clearForm() } }

function clearForm() { newTemplate.value = { id: '', name: '', templateText: '', sections: [] } }

const handleDelete = function () { if (deleteEnabled.value) { delete templates.value[selectedTemplateKey.value] clearForm() } }

const deleteEnabled = computed(() => { return Object.hasOwn(templates.value, selectedTemplateKey.value) })

const saveEnabled = computed(() => { return newTemplate.value.id && newTemplate.value.name && newTemplate.value.templateText })

watch(selectedTemplate, async () => { newTemplate.value = selectedTemplate.value }) </script>

<template> <div id="template-editor-view-content" class="row p-1 pt-4"> <div class="col"> <div class="row"> <div class="col"><h1>Template Editor</h1></div> </div> <div class="row"> <div id="options-column" class="col-4 primary-bordered vh-85"> <div class="row pt-3"> <div class="col"><h2 class="text-center">Template Options</h2></div> </div> <TemplateSelectorComponent @selected-template-changed="updateSelectedTemplateKey" /> <div class="row"> <div class="col"> <h3 class="text-center">Global Variables</h3> </div> </div> <div class="row"> <div class="col"> <div id="globalVarsAccordion" class="accordion"> <div class="accordion-item"> <h2 id="companyNameAccordionHeader" class="accordion-header"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#companyNameAccordionBody" aria-expanded="false" aria-controls="companyNameAccordionBody" > Company Name </button> </h2> <div id="companyNameAccordionBody" class="accordion-collapse collapse" aria-labelledby="companyNameAccordionHeader" data-bs-parent="#globalVarsAccordion" > <div class="accordion-body"> <ul class="list-group list-group-horizontal"> <li class="list-group-item">isSelected</li> <li v-pre class="list-group-item">{{ companyName.isSelected }}</li> </ul> <ul class="pt-2 list-group list-group-horizontal"> <li class="list-group-item">value</li> <li v-pre class="list-group-item">{{ companyName.value }}</li> </ul> </div> </div> </div> <div class="accordion-item"> <h2 id="jobTitleAccordionHeader" class="accordion-header"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#jobTitleAccordionBody" aria-expanded="false" aria-controls="jobTitleAccordionBody" > Job Title </button> </h2> <div id="jobTitleAccordionBody" class="accordion-collapse collapse" aria-labelledby="jobTitleAccordionHeader" data-bs-parent="#globalVarsAccordion" > <div class="accordion-body"> <ul class="list-group list-group-horizontal"> <li class="list-group-item">isSelected</li> <li v-pre class="list-group-item">{{ jobTitle.isSelected }}</li> </ul> <ul class="pt-2 list-group list-group-horizontal"> <li class="list-group-item">value</li> <li v-pre class="list-group-item">{{ jobTitle.value }}</li> </ul> </div> </div> </div> </div> </div> </div> <template v-if="newTemplate.sections"> <div class="row"> <div class="col"> <h3 class="text-center">Template Variables</h3> </div> </div> <div class="row"> <div class="col"> <div id="templateVarsAccordion" class="accordion"> <div v-for="section in newTemplate.sections" :key="section.key" class="accordion-item" > <h2 id="${section.key}AccordionHeader" class="accordion-header"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#${section.key}AccordionBody" aria-expanded="false" aria-controls="${section.key}AccordionBody" > {{ section.label }} </button> </h2> <div id="${section.key}AccordionBody" class="accordion-collapse collapse" aria-labelledby="${section.key}AccordionHeader" data-bs-parent="#templateVarsAccordion" > <div class="accordion-body"> <ul class="list-group list-group-horizontal"> <li class="list-group-item">isSelected</li> <li class="list-group-item"> <span v-pre>{{</span> {{ section.key }}.isSelected <span v-pre>}}</span> </li> </ul> <ul class="pt-2 list-group list-group-horizontal"> <li class="list-group-item">value</li> <li class="list-group-item"> <span v-pre>{{</span> {{ section.key }}.value <span v-pre>}}</span> </li> </ul> </div> </div> </div> </div> </div> </div> </template> </div> <div id="working-area" class="col vh-85"> <form class="row gx-3 align-items-center pb-3 pt-2"> <div class="col-auto"> <label for="templateKeyInput" class="col-form-label-lg">Template ID</label> </div> <div class="col"> <input id="templateKeyInput" v-model="newTemplate.id" type="text" class="form-control form-control-lg" /> </div> <div class="col-auto"> <label for="templateNameInput" class="col-form-label-lg">Template Name</label> </div> <div class="col"> <input id="templateNameInput" v-model="newTemplate.name" type="text" class="form-control form-control-lg" /> </div> </form> <form class="row text-center justify-content-center"> <div class="col-2"> <button class="btn btn-secondary" @click="showModal = true">Add Section</button> </div> <div class="col-2"> <button class="btn btn-success" :disabled="!saveEnabled" @click="handleSave"> Save Template </button> </div> <div class="col-2"> <button class="btn btn-danger" :disabled="!deleteEnabled" @click="handleDelete"> Delete Template </button> </div> </form> <div class="row pt-3"> <div class="col"> <textarea id="templateTextArea" v-model="newTemplate.templateText" class="form-control" ></textarea> </div> </div> </div> </div> </div> <AddSectionModal :show="showModal" @close="showModal = false" @submit="handleModalSubmit" /> </div> </template>

<style scoped> // Some css </style>

The modal which is being created is defined as follows (AddSectionModal.vue):

<script setup>
import GenericModal from './GenericModal.vue'
import { computed, ref } from 'vue'

defineProps({ show: Boolean }) const emit = defineEmits(['close', 'submit'])

const section = ref({ key: '', label: '', text: '', isSelected: false })

function clearState() { section.value = { key: '', label: '', text: '', isSelected: false } }

const submitEnabled = computed(() => { return section.value.key && section.value.text && section.value.label })

const handleClose = function () { clearState() emit('close') }

const handleSubmit = function () { if (submitEnabled.value) { const value = section.value clearState() emit('submit', value) } } </script>

<template> <Teleport to="body"> <GenericModal :show="show"> <template #header> <h5 class="mx-auto">Add New Section</h5> </template>

<template #body> <div class="container"> <div class="row gx-3 align-items-center"> <div class="col-3"> <label for="sectionKey" class="col-form-label-lg">Section Key</label> </div> <div class="col"> <input v-model="section.key" name="sectionKey" type="text" class="form-control form-control-lg" /> </div> </div> <div class="row pt-2 gx-3 align-items-center"> <div class="col-3"> <label for="sectionLabel" class="col-form-label-lg">Section Label</label> </div> <div class="col"> <input v-model="section.label" name="sectionLabel" type="text" class="form-control form-control-lg" /> </div> </div> <div class="row pt-2 gx-3 align-items-center"> <div class="col-3"> <label for="sectionText" class="col-form-label-lg">Section Text</label> </div> <div class="col"> <input v-model="section.text" name="sectionText" type="text" class="form-control form-control-lg" /> </div> </div> </div> </template>

<template #footer> <div class="row gx-1 float-end"> <div class="col"> <button class="btn btn-danger" @click="handleClose">Cancel</button> </div> <div class="col"> <button class="btn btn-success" :disabled="!submitEnabled" @click="handleSubmit"> Submit </button> </div> </div> </template> </GenericModal> </Teleport> </template>

And the GenericModal implementation is as follows (GenericModal.vue):

<template>
    <Transition name="modal">
        <div v-if="show" class="modal-mask">
            <div class="modal-container">
                <div class="modal-header">
                    <slot name="header">default header</slot>
                </div>

<div class="modal-body"> <slot name="body">default body</slot> </div>

<div class="modal-footer"> <slot name="footer"> default footer <button class="modal-default-button" @click="$emit('close')">OK</button> </slot> </div> </div> </div> </Transition> </template>

<style> // Some CSS </style>

What strategies could I use to track down the source of the unexpected reactivity?

Scroll to top