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