Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/composable-tovalue-inside-watcheffect.md
1---2title: Call toValue() Inside watchEffect for Proper Dependency Tracking3impact: HIGH4impactDescription: Calling toValue() outside watchEffect prevents reactive dependency tracking, causing the effect to never re-run5type: gotcha6tags: [vue3, composables, composition-api, watchEffect, toValue, reactivity]7---89# Call toValue() Inside watchEffect for Proper Dependency Tracking1011**Impact: HIGH** - When writing composables that accept `MaybeRefOrGetter` arguments, you must call `toValue()` inside the `watchEffect` callback, not outside. If you extract the value before the watchEffect, Vue cannot track the dependency and the effect will never re-run when the source changes.1213This is a subtle but critical mistake that leads to composables that work with initial values but never update.1415## Task Checklist1617- [ ] Always call `toValue()` inside `watchEffect` callbacks, not before18- [ ] Similarly, access `.value` on refs inside watchEffect, not outside19- [ ] For `watch()`, use a getter function that calls `toValue()`20- [ ] Test that composables update when their inputs change2122**Incorrect:**23```javascript24import { ref, watchEffect, toValue } from 'vue'2526export function useFetch(url) {27const data = ref(null)28const error = ref(null)2930// WRONG: toValue called outside watchEffect31// This extracts the value ONCE and passes a static string32const urlValue = toValue(url)3334watchEffect(async () => {35try {36// urlValue is a static string - no dependency tracked!37const response = await fetch(urlValue)38data.value = await response.json()39} catch (e) {40error.value = e41}42})4344return { data, error }45}4647// When used like this:48const apiUrl = ref('/api/users')49const { data } = useFetch(apiUrl)5051// Later...52apiUrl.value = '/api/products' // useFetch will NOT refetch!53```5455**Correct:**56```javascript57import { ref, watchEffect, toValue } from 'vue'5859export function useFetch(url) {60const data = ref(null)61const error = ref(null)6263watchEffect(async () => {64// CORRECT: toValue called INSIDE watchEffect65// Vue tracks this as a dependency66const urlValue = toValue(url)6768try {69const response = await fetch(urlValue)70data.value = await response.json()71} catch (e) {72error.value = e73}74})7576return { data, error }77}7879// Now when used:80const apiUrl = ref('/api/users')81const { data } = useFetch(apiUrl)8283// Later...84apiUrl.value = '/api/products' // useFetch WILL refetch!85```8687## The Same Applies to Direct Ref Access8889```javascript90// WRONG: Accessing .value outside the effect91export function useDebounce(source, delay = 300) {92// This captures the initial value, not a reactive dependency93const initialValue = source.value // or toValue(source)9495watchEffect(() => {96// initialValue is static - this only runs once97console.log('Value:', initialValue)98})99}100101// CORRECT: Access inside the effect102export function useDebounce(source, delay = 300) {103watchEffect(() => {104// Vue tracks source.value or toValue(source) as dependency105console.log('Value:', toValue(source))106})107}108```109110## Pattern: Using watch() with Getter Functions111112For `watch()`, wrap `toValue()` in a getter:113114```javascript115import { ref, watch, toValue } from 'vue'116117export function useLocalStorage(key, defaultValue) {118const data = ref(defaultValue)119120// CORRECT: Use getter function with watch121watch(122() => toValue(key), // Getter calls toValue, tracks dependency123(newKey) => {124const stored = localStorage.getItem(newKey)125data.value = stored ? JSON.parse(stored) : defaultValue126},127{ immediate: true }128)129130return data131}132```133134## Why This Happens135136Vue's reactivity tracking works by detecting property accesses during effect execution:137138```javascript139watchEffect(() => {140// When this runs, Vue is "recording" what reactive sources are accessed141const value = someRef.value // Vue records: "this effect depends on someRef"142})143144// But if you extract the value before:145const value = someRef.value // Vue isn't recording yet146watchEffect(() => {147console.log(value) // Just using a plain JavaScript variable148})149```150151`toValue()` works the same way - it accesses `.value` internally, so it must happen during effect execution for tracking to work.152153## Quick Checklist for Composable Authors154155When accepting `MaybeRefOrGetter` inputs:1561571. Store the raw argument (don't call `toValue` during setup)1582. Call `toValue()` inside any reactive context (`watchEffect`, `watch`, `computed`)1593. Test with both static values AND refs that change160161```javascript162export function useMyComposable(input) {163// Store raw - don't extract value here164// const value = toValue(input) // WRONG165166const result = computed(() => {167// Extract value inside reactive context168return transform(toValue(input)) // CORRECT169})170171watchEffect(() => {172// Extract value inside reactive context173doSomething(toValue(input)) // CORRECT174})175176return { result }177}178```179180## Reference181- [Vue.js Reactivity API - toValue](https://vuejs.org/api/reactivity-utilities.html#tovalue)182- [Vue.js Composables - Accepting Ref Arguments](https://vuejs.org/guide/reusability/composables.html#accepting-reactive-state)183