A dashboard panel calls VueUse's useFetch(`/api/stats?range=${props.range}`) and refDebounced on props.search; why does neither react when the props change, and how should you pass them?
answer
- a snapshot taken during setup
- MaybeRefOrGetter parameters
- refetch is off by default
- some parameters want a real Ref
- the newest request wins
basics
~20 sBoth calls receive plain values read once during setup. Pass useFetch a getter URL plus refetch: true, since refetch defaults to false; refDebounced accepts only a Ref, so wrap the prop with toRef(() => props.search).
solid answer
~40 sA template literal or `props.search` evaluated in `setup` is a plain value — a snapshot. VueUse keeps inputs reactive by typing most parameters as `MaybeRefOrGetter` and reading them with `toValue` inside its own watchers, but that only works if you hand over a ref or a getter. For `useFetch`, pass `() => `/api/stats?range=${props.range}`` **and** `refetch: true`: the URL is watched, but a change triggers a new request only when `refetch` is on, and it defaults to false. Chain `.json()` to get parsed data rather than text. `refDebounced`'s source is typed `Ref<T>`, not `MaybeRefOrGetter`, so a getter does not type-check; `toRef(() => props.search)` turns the prop into a read-only ref it can watch. When a refetch starts while a request is in flight, `useFetch` aborts the older one and only the newest may write `data`.
code
vue · 19 lines<script setup lang="ts">
import { toRef } from 'vue'
import { refDebounced, useFetch } from '@vueuse/core'
const props = defineProps<{ range: string; search: string }>()
// broken: both arguments are strings captured once during setup
// useFetch(`/api/stats?range=${props.range}`)
// refDebounced(props.search, 300)
// fixed: a getter URL, refetch on, parsed as JSON
const { data, isFetching } = useFetch(
() => `/api/stats?range=${props.range}`,
{ refetch: true },
).json<StatsResponse>()
// fixed: refDebounced needs a Ref, so wrap the prop getter
const debouncedSearch = refDebounced(toRef(() => props.search), 300)
</script>go deeper
Recall that passing props.search reads a value once; to stay reactive, pass a ref or a getter such as () => props.search.
Explain MaybeRefOrGetter at VueUse call sites and why useFetch also needs refetch: true to request again when its URL changes.
Show you read signatures before trusting reactivity: Ref-only parameters, plain-only options such as threshold, and useFetch's abort-and-latest-wins behaviour.
Set a team convention for composable inputs — getters for props, explicit refetch policies — so reactivity bugs are caught in review, not production.
## The call-site problem In `setup`, the expression `` `/api/stats?range=${props.range}` `` is evaluated once and produces a string. Passing that string to any function hands over a **snapshot**; when the parent later changes `range`, nothing re-runs the template literal. The same holds for `props.search`: reading it yields the current string, not a connection to the prop. This is the most common way VueUse composables "stop working" in real components, and it is a call-site mistake, not a library bug. ## What MaybeRefOrGetter means in VueUse Vue 3.3 introduced the `MaybeRefOrGetter<T>` type — a plain `T`, a `Ref<T>`, or a getter `() => T` — and VueUse uses it for most inputs. Inside, the composable reads the argument with Vue's `toValue` within a `computed` or watcher, so: - a **ref** argument is tracked, and changes flow through; - a **getter** argument is tracked too, and is the idiomatic way to pass a prop or a derived expression; - a **plain value** is read once and never changes. The mechanics of `toValue` belong to Vue itself; what matters here is knowing **which** VueUse parameters accept which forms. ## Not every parameter is reactive | Function | Parameter | Accepts | |---|---|---| | `useFetch` | `url` | `MaybeRefOrGetter<string>` | | `useFetch` | `refetch` option | `MaybeRefOrGetter<boolean>`, default `false` | | `refDebounced` | `value` | `Ref<T>` only | | `refDebounced` | `ms` | `MaybeRefOrGetter<number>`, default 200 | | `useStorage` | `key` | `MaybeRefOrGetter<string>` | | `useEventListener` | target, event, options | `MaybeRefOrGetter`; the listener is `MaybeRef` | | `useIntersectionObserver` | `rootMargin` | `MaybeRefOrGetter<string>` | | `useIntersectionObserver` | `threshold` | plain number or array, read once | Two lessons follow. First, read the signature: a getter passed where `Ref<T>` is required fails type-checking, and a ref passed where a plain value is read is silently snapshotted. Second, a reactive argument is not the same as a reactive **behaviour**: `useFetch` tracks its URL but will not request again unless `refetch` is true. ## Fixing the panel ```ts const props = defineProps<{ range: string; search: string }>() const { data, isFetching, error } = useFetch( () => `/api/stats?range=${props.range}`, { refetch: true }, ).json<StatsResponse>() const debouncedSearch = refDebounced(toRef(() => props.search), 300) ``` `toRef(() => props.search)` creates a read-only ref backed by the getter, which satisfies `refDebounced`'s `Ref<T>` parameter while staying live. Calling `toRefs(props)` and passing `search` would work as well. ## What useFetch does when the URL changes 1. With `refetch: false` (the default), the URL change is observed and ignored; call `execute()` yourself if you want a request. 2. With `refetch: true`, the change triggers `execute()`. 3. `execute()` first aborts any in-flight request through its `AbortController`, then resets the returned state — `error`, `statusCode` and `aborted` — for the new request. 4. An internal counter ensures only the **latest** execution writes `data`, `error` and `statusCode`, so a slow earlier response cannot overwrite a newer one. 5. The body is read as text unless you chain `.json()` (or `.blob()`, `.arrayBuffer()`, `.formData()`); without `.json()`, `data` holds a string. 6. A non-2xx response sets `error` and resets `data` to `initialData` or `null`, even though the underlying `fetch` resolves for HTTP errors. In a Nuxt app, the `@vueuse/nuxt` module deliberately leaves VueUse's `useFetch` out of auto-imports, because Nuxt ships its own `useFetch` with a different API; import VueUse's explicitly from `@vueuse/core`. ## Checklist for a VueUse call - Pass props and derived values as **getters**, not evaluated expressions. - Check whether the parameter is `MaybeRefOrGetter`, `Ref` or plain. - Check whether a reactive input also needs an **option** to act on it, such as `refetch`. - Choose the response type explicitly with `.json()` when you expect JSON. ## Why getters rather than refs everywhere VueUse introduced the getter convention in version 9 because much reactive data in a component is not a standalone ref: props, properties of a `reactive()` object, and expressions combining several sources. Turning each into a ref first adds ceremony; a getter such as `() => props.range` expresses the dependency directly and costs nothing. Refs remain the right input when the value already is one, or when the caller also needs to write it. Parameters typed `Ref<T>` — like `refDebounced`'s source — are the exception to learn, not the rule, and the type checker points them out as long as the component is written in TypeScript.
- Why does VueUse's useFetch with a getter URL still not refetch when refetch is left at its default?The getter makes the URL reactive, but `useFetch` only calls `execute()` on a URL change when `refetch` is truthy, and it defaults to `false`. Either pass `refetch: true` (itself allowed to be a ref or getter) or call `execute()` from your own watcher.
- How does VueUse's useFetch stop a slow earlier response from overwriting a newer one?Each `execute()` aborts the previous request through an `AbortController` and increments an internal counter. When a response arrives, it writes `data`, `error` and `statusCode` only if its counter still matches the latest execution, so stale results are discarded.
saying these in an interview costs you the question
- VueUse composables track any variable you pass, even a plain string.
- A getter URL makes VueUse's useFetch refetch without any option.
- refDebounced accepts a getter like most VueUse parameters.
- useFetch's data holds parsed JSON unless you ask for text.
- Overlapping useFetch refetches can leave data from the older request.