Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

dcranmer's avatar

Vue 3, Inertia props, refs don't update

I've got an Inertia app where an event emitted by a child component (which manages a Details/Summary disclosure element) does not reliably update both a Vue ref or supposedly reactive Inertia props. Sometimes the Vue ref will update in Vue Devtools if I click Refresh. The Inertia props do not.

When the disclosure triangle in the child component is clicked, it emits a 'project-toggle' event to the parent with a data object. Opening the disclosure should both add an item to the isExpanded array and change the props.data.projects.expanded property to true. Closing the disclosure does the reverse.

When the disclosure is expanded, the event gets sent correctly, and the handleProjectToggle function logs the changed item to the console, with the expanded property equaling true:

{"id":5,"project_name":"Towards a Sustainable Agriculture - A Curriculum for High School Classes","status_id":3,"description":"The Center for Integrated Agricultural Systems has developed a curriculum on sustainable agriculture for use in high school classes.  The curriculum focuses on the importance of citizen and consumer choices as well as agricultural practices that can help the environment and communities along with improving farm profits.  The curriculum provides case studies and classroom activities, as well as background information and additional resources for teachers to use.","website":"https://www.cias.wisc.edu/curriculum-new/","school":{"id":1,"school_name":null,"display_name":"College of Agricultural and Life Sciences"},"leader":{"id":7,"first_name":"Diane","last_name":"Mayerfeld","name":"Diane Mayerfeld"},"expanded":true}

But the same item in props.projects.data remains unchanged in Vue Devtools, even when refreshed.

projects:Object
data:Array[50]
0:Object
description:"The Center for Integrated Agricultural Systems has developed a curriculum on sustainable agriculture for use in high school classes.  The curriculum focuses on the importance of citizen and consumer choices as well as agricultural practices that can help the environment and communities along with improving farm profits.  The curriculum provides case studies and classroom activities, as well as background information and additional resources for teachers to use."
expanded:false
id:5
leader:Object
project_name:"Towards a Sustainable Agriculture - A Curriculum for High School Classes"
school:Object
status_id:3
website:"https://www.cias.wisc.edu/curriculum-new/"

Why are these values not the same?

Parent component:

<script setup>
import { computed, onMounted, ref } from 'vue'
import { usePage } from '@inertiajs/vue3'
const { props } = usePage()
const projects = computed(() => props.projects)
const isExpanded = ref([])

const handleProjectToggle = (obj) => {
  props.projects.data?.map(item => {
    if (item.id === obj.id) {
      if (obj.open) {
        item.expanded = true
        isExpanded.value.push(item.id)
        console.log(JSON.stringify(item))
      }
      item.expanded = false
      isExpanded.value.filter(item => item.id !== obj.id)
    }
  })
}
</script>
<template>
<ul>

            <li
              v-for="(project, index) of props.projects.data"
              :id="`result-${index}`"
              :key="`${index}-${project.id}`"
            >
              <ProjectResultsDetailsNew
                @project-toggle="handleProjectToggle"
                :index="index"
                :project="project"
                />
</li>
</ul>
</template>

Child component:

<script setup>
import { defineProps, defineEmits, watch, computed, ref } from 'vue'

  const props = defineProps( {
    project: {
      type: Object
    },
  })

const expanded = computed(() => props.project.expanded)
const open =  ref(false)

  watch(expanded,(newValue, oldValue) =>
    newValue
      ? open.value = true
      : !open.value
  )

const emit = defineEmits(
  ['project-toggle']
)

    // default toggle event behavior is prevented and replaced by $emit for parent to handle
    const handleToggle = (e) => {
      emit('project-toggle', {
        open: e.target.open,
        index: props.index,
        id: props.project.id}
      )
      open.value = !open.value
    }
    const openStatus = () => open.value

</script>
<template>
<template>
  <details ref="details" :open="open" :class="type" @toggle.prevent="handleToggle($event)">

    <summary :id="`project-name-${project.id}`" class="project-name" >
      <slot name="summary">
        {{ project.project_name }}
      </slot>
    </summary>

    <div class="project-meta">
      <div class="meta leader">
        <slot name="leaderName">
          {{ project.leader.name }}
        </slot>
      </div>
      <div :id="`school-name-${project.id}`" class="meta">
        <slot name="school">
          {{ project.school.display_name }}
        </slot>
      </div>
      <slot name="profileLink">
        <a class="meta full-profile" :href="route('project.show', {project: project.id})" :aria-describedby="`project-name-${project.id}`">
          View profile</a>
      </slot>
    </div>
    <div class="details-content">
      <h3>Description</h3>
      <div class="widb-result-description">
        <slot name="description">
          {{ project.description }}
        </slot>
      </div>
    </div>
  </details>
</template>
</template>

0 likes
2 replies
LaryAI's avatar
Level 58

The issue seems to be with how you are updating the expanded property in the parent component. In the handleProjectToggle function, you are not correctly updating the isExpanded ref and the expanded property in the props.projects.data array.

To fix this, you can modify the handleProjectToggle function as follows:

const handleProjectToggle = (obj) => {
  props.projects.data?.forEach((item) => {
    if (item.id === obj.id) {
      if (obj.open) {
        item.expanded = true
        isExpanded.value.push(item.id)
        console.log(JSON.stringify(item))
      } else {
        item.expanded = false
        isExpanded.value = isExpanded.value.filter((id) => id !== item.id)
      }
    }
  })
}

In the child component, you can simplify the watch statement and the handleToggle function:

watch(expanded, (newValue) => {
  open.value = newValue
})

const handleToggle = () => {
  emit('project-toggle', {
    open: open.value,
    index: props.index,
    id: props.project.id
  })
}

These changes should ensure that both the isExpanded ref and the expanded property in props.projects.data are updated correctly.

Let me know if this helps!

Please or to participate in this conversation.