Files
agent-skills/skills/vue-best-practices/reference/prop-composable-reactivity-loss.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

4.4 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Preserve Reactivity When Passing Props to Composables HIGH Passing prop values directly to composables loses reactivity - composable won't update when props change gotcha
vue3
props
composables
reactivity
composition-api

Preserve Reactivity When Passing Props to Composables

Impact: HIGH - A common mistake is passing data received from a prop directly to a composable. This passes the current value, not a reactive source. When the prop updates, the composable won't receive the new value, leading to stale data.

This is one of the most frequent sources of "my composable doesn't update" bugs in Vue 3.

Task Checklist

  • Pass props to composables via computed properties or getter functions
  • Use toRefs() when passing multiple props to maintain reactivity
  • In composables, use toValue() to normalize inputs that may be getters or refs
  • Test that composable output updates when props change

Incorrect:

<script setup>
import { useFetch } from './composables/useFetch'
import { useDebounce } from './composables/useDebounce'

const props = defineProps({
  userId: Number,
  searchQuery: String
})

// WRONG: Passes initial value, not reactive source
// useFetch will never refetch when userId changes!
const { data } = useFetch(`/api/users/${props.userId}`)

// WRONG: Debounced value is frozen at initial searchQuery
const debouncedQuery = useDebounce(props.searchQuery, 300)
</script>

Correct:

<script setup>
import { computed } from 'vue'
import { useFetch } from './composables/useFetch'
import { useDebounce } from './composables/useDebounce'

const props = defineProps({
  userId: Number,
  searchQuery: String
})

// CORRECT: Use computed to create reactive URL
const userUrl = computed(() => `/api/users/${props.userId}`)
const { data } = useFetch(userUrl)

// CORRECT: Pass getter function to preserve reactivity
const debouncedQuery = useDebounce(() => props.searchQuery, 300)
</script>

Pattern: Using toRefs for Multiple Props

<script setup>
import { toRefs } from 'vue'
import { useUserForm } from './composables/useUserForm'

const props = defineProps({
  initialName: String,
  initialEmail: String,
  initialAge: Number
})

// Convert all props to refs, preserving reactivity
const { initialName, initialEmail, initialAge } = toRefs(props)

// Now each is a ref that tracks prop changes
const { form, isValid } = useUserForm({
  name: initialName,
  email: initialEmail,
  age: initialAge
})
</script>

Writing Reactivity-Safe Composables

Composables should accept multiple input types using toValue():

// composables/useDebounce.js
import { ref, watch, toValue } from 'vue'

export function useDebounce(source, delay = 300) {
  // toValue() handles: ref, getter function, or plain value
  const debounced = ref(toValue(source))
  let timeout

  watch(
    () => toValue(source),  // Normalizes any input type
    (newValue) => {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        debounced.value = newValue
      }, delay)
    },
    { immediate: true }
  )

  return debounced
}
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    error.value = null

    try {
      // toValue() makes this work with computed, getter, or string
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}

Quick Reference: Input Types

Input to Composable Reactive? Example
props.value No useFetch(props.userId)
computed(() => ...) Yes useFetch(computed(() => props.userId))
() => props.value Yes* useFetch(() => props.userId)
toRef(props, 'key') Yes useFetch(toRef(props, 'userId'))
toRefs(props).key Yes const { userId } = toRefs(props); useFetch(userId)

*Requires composable to use toValue() internally

Reference