skip to content

How does a Vue 3 composable differ from a React custom hook in when its code runs and how it tracks dependencies?

level: middleimportance: should knowfreq 52%

answer

  1. same idea, different execution model
  2. setup runs how many times?
  3. no dependency arrays
  4. no stale closures over refs
  5. condition decided once at setup

basics

~20 s

A Vue composable runs once per component, during setup, and its computed values and watchers track dependencies automatically; a React custom hook re-runs on every render and relies on call order and explicit dependency arrays.

solid answer

~40 s

Both package stateful logic into composable `use` functions, and the Composition API was partly inspired by React hooks. The difference is the execution model. In Vue, `setup()` or `<script setup>` runs **once** per component instance, so a composable's body runs once; its refs keep the same identity for the component's life, so closures over them never go stale. `computed()`, `watchEffect()` and the render effect record what they read and re-run only when that changes, so there are no dependency arrays. In React's public model the component body and its hooks re-run on every render, hooks are matched by call order, and effects and memoised values list their dependencies explicitly. Vue's own constraint is different: call composables synchronously during setup.

code

ts · 17 lines
ts
import { ref, computed, watch } from 'vue'

export function useCounter(step = 1) {
  // Runs once per component: these refs keep their identity for the component's life.
  const count = ref(0)
  const double = computed(() => count.value * 2) // no dependency list: count is tracked on read

  function increment() {
    count.value += step // never stale: reads and writes the same ref every time
  }

  watch(count, (n) => {
    if (n > 100) count.value = 0
  })

  return { count, double, increment }
}

go deeper

for a junior

Recall the headline: in Vue, setup and composables run once per component and dependencies are tracked automatically.

for a middle

Explain why refs avoid stale closures, why no dependency arrays exist, and why a conditional composable call is decided once.

for a senior

Show how the model changes design: reactive inputs as getters, no memoisation work, and the synchronous-call-during-setup constraint that remains.

for a principal

Weigh the two models for a team moving between frameworks: which habits transfer, which cause bugs, and how to review for them.

## Two models that look alike Vue's Composition API was partly inspired by React hooks, and a composable such as `useCounter()` looks like a custom React hook: a `use`-prefixed function that creates state and returns it. The resemblance ends at the execution model, and interviewers use the comparison to check whether a candidate understands Vue's. ## How often the code runs - **React, public model:** a function component's body runs on every render, and so does every hook it calls. React matches each hook call to its stored state by the order of the calls, which is why hooks may not be called conditionally. Closures created during one render see that render's values. - **Vue:** `setup()`, or the top level of `<script setup>`, runs **once per component instance**. A composable called there runs once. Its refs are ordinary variables captured in closures, and because the ref object is the same for the component's whole life and `.value` always reads the current value, those closures do not go stale. Re-rendering the component re-runs only its render function, not setup. ## How dependencies are tracked - **React:** effects and memoised values take an explicit **dependency array**. Getting it wrong produces stale values or extra runs, and a lint rule is commonly used to police it. - **Vue:** `computed()`, `watchEffect()` and each component's render effect record which reactive values they read while running, and re-run only when one of those changes. There is no dependency list to maintain. `watch()` does take an explicit *source*, but that is a choice of what to observe, not a list that must be kept in sync with the body. ## Consequences for writing composables | Concern | Vue composable | React custom hook | |---|---|---| | Body execution | once, during setup | on every render | | Stale closures | not an issue for refs | a common bug class | | Dependency lists | none; tracked automatically | written by hand for effects and memoisation | | Conditional call | allowed; evaluated once at setup | not allowed | | Memoising callbacks passed to children | rarely needed | often needed to avoid child re-renders | The last row follows from fine-grained tracking: a Vue child component re-renders when reactive state it read changes, or when its props change, not merely because a new function object was created in the parent on every render, since the parent's setup code does not re-run. ## Where Vue has its own rules Running once does not mean "call it anywhere". A composable must still be called **synchronously during setup** (or inside a lifecycle hook), because that is when Vue knows which component instance is active and can attach the composable's lifecycle hooks and watchers to it. Call it from a click handler and its hooks are not registered. And because setup runs once, a condition around a composable call is evaluated once: `if (props.compact) useResize()` decides at creation time and does not re-evaluate if the prop later changes. When behaviour must follow a changing prop, pass the prop in as a getter and let the composable watch it. ## How to answer in an interview 1. Name the shared idea: both extract stateful logic into `use` functions that compose. 2. State Vue's execution model: setup and the composables it calls run once; refs hold current state. 3. State Vue's tracking model: automatic dependency collection, no dependency arrays. 4. Name the consequences: no stale closures over refs, no call-order rule, little manual memoisation. 5. Name Vue's own constraint: a synchronous call during setup, and conditions that are decided once. Describe React at the level of its public model only. The point of the question is to show you know what Vue does, not to recite React internals.

  • If Vue composables run once, how does a composable react when a prop changes later?
    Through reactive inputs. The component passes a getter such as `() => props.id` or a ref, and the composable reads it inside `watch()`, `watchEffect()` or `computed()`. Those effects re-run on change; the composable's body does not.
  • Can a Vue composable be called conditionally inside setup?
    Yes, because Vue does not match calls by order. But the condition is evaluated once, when setup runs. If the condition depends on a prop that can change, move the condition inside the composable's watcher or computed rather than around the call.

saying these in an interview costs you the question

  • Vue composables re-run on every render, just like React hooks.
  • computed() in Vue needs a dependency array to know when to recompute.
  • Vue composables must be called in the same order on every render.
  • Vue handlers returned by a composable need memoising to avoid stale state.
  • Because setup runs once, a composable can be called from any callback.