---
title: Preserve Reactivity When Passing Props to Composables
impact: HIGH
impactDescription: Passing prop values directly to composables loses reactivity - composable won't update when props change
type: gotcha
tags: [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:**
```vue
```
**Correct:**
```vue
```
## Pattern: Using toRefs for Multiple Props
```vue
```
## Writing Reactivity-Safe Composables
Composables should accept multiple input types using `toValue()`:
```javascript
// 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
}
```
```javascript
// 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
- [Vue.js Reactivity API - toValue](https://vuejs.org/api/reactivity-utilities.html#tovalue)
- [Vue.js Composables - Conventions and Best Practices](https://vuejs.org/guide/reusability/composables.html#conventions-and-best-practices)