Tag: Composition API

  • Vue 3 Reactivity: State, Derived Values and Effects Without Surprises

    Vue 3 Reactivity: State, Derived Values and Effects Without Surprises

    Vue's reactivity system connects state reads to the effects that depend on them. When reactive state changes, Vue can update the relevant component render and invalidate derived values. This removes a great deal of manual DOM synchronisation, but it does not remove the need to decide what the source of truth is, which values are derived and where side effects belong.

    In Vue 3, reactive objects use JavaScript Proxy, while refs use getter and setter access around .value. Dependency tracking happens at runtime as an effect reads reactive properties (Vue — Reactivity in Depth).

    Article map for Vue 3 Reactivity: State, Derived Values and Effects Without Surprises, covering Separate state, derived values and effects, Choose ref and reactive deliberately, Use watchers for effects, not duplicate d…
    Article map: Separate state, derived values and effects; Choose ref and reactive deliberately; Use watchers for effects, not duplicate derivation; Keep state close until it is genuinely shared.

    Separate state, derived values and effects

    A maintainable component usually contains three distinct ideas:

    • State: the smallest mutable source of truth, such as a search term or selected identifier.
    • Derived value: information calculated from state, such as a filtered list or total.
    • Effect: work outside pure derivation, such as a network request, persistence, analytics or focus management.

    Use ref() or reactive() for state, computed() for a derived value and watch() or watchEffect() for a side effect. If the same value can be calculated, storing a second mutable copy creates synchronisation bugs.

    <script setup>
    import { computed, ref, watch } from 'vue';
    
    const query = ref('');
    const records = ref([]);
    
    const visibleRecords = computed(() => {
      const needle = query.value.trim().toLowerCase();
      return needle
        ? records.value.filter((record) => record.name.toLowerCase().includes(needle))
        : records.value;
    });
    
    watch(query, (value) => {
      // Example side effect: update a URL or schedule a request.
      // Keep cancellation and error handling explicit in production code.
      console.debug('Query changed', value);
    });
    </script>

    computed() caches its result based on reactive dependencies and should remain free of side effects. A computed getter that writes state, starts a request or changes the DOM is difficult to reason about and may run at unexpected times.

    Choose ref and reactive deliberately

    ref() can hold primitives or objects and exposes the value as .value in JavaScript. Templates generally unwrap top-level refs. It is often a simple default because the container can be replaced without losing the reactive connection.

    reactive() returns a proxy for an object. It is convenient for related fields, but the proxy identity differs from the original object, and replacing the whole variable with another raw object disconnects code that still references the previous proxy.

    A common pitfall is destructuring a primitive property from a reactive object:

    const state = reactive({ count: 0 });
    const { count } = state;
    
    state.count += 1;
    console.log(count); // The local binding is not reactive.

    Use toRef() or toRefs() when a property must remain linked through destructuring, or keep property access on the reactive object. Vue's documentation explains that destructured local bindings no longer trigger the source proxy's get/set traps (Vue — Reactivity in Depth).

    Do not mix raw and proxied versions as map keys or identity tokens without understanding the consequences. Use stable business identifiers for records rather than object identity when possible.

    Use watchers for effects, not duplicate derivation

    watch(source, callback) tracks an explicit source and gives access to current and previous values. watchEffect(callback) runs immediately and discovers dependencies while its synchronous body executes. An async watchEffect only tracks dependencies accessed before the first await, so explicit sources can be clearer for requests.

    Watchers are appropriate for:

    • fetching when an identifier changes;
    • synchronising selected state to the URL;
    • integrating with a non-Vue library;
    • persisting a bounded preference; and
    • triggering an accessible notification or focus change after a state transition.

    They are usually not needed to calculate display values. Use a computed property for that.

    Network effects need cancellation so a slow response for an old query does not overwrite a newer result. Vue watcher cleanup can abort stale work:

    watch(selectedId, async (id, _previous, onCleanup) => {
      if (!id) return;
    
      const controller = new AbortController();
      onCleanup(() => controller.abort());
    
      const response = await fetch(`/api/records/${encodeURIComponent(id)}`, {
        signal: controller.signal,
      });
    
      if (!response.ok) throw new Error(`Request failed: ${response.status}`);
      record.value = await response.json();
    });

    Production code should also expose loading, empty, failure and retry states and distinguish an intentional abort from a user-visible error.

    Decision path for Vue 3 Reactivity: State, Derived Values and Effects Without Surprises, covering Use watchers for effects, not duplicate derivation, Keep state close until it is genuinely shared, Avoid cross-request st…
    Decision path: Use watchers for effects, not duplicate derivation; Keep state close until it is genuinely shared; Avoid cross-request state leakage in SSR; Optimise only after profiling updates.

    Keep state close until it is genuinely shared

    Local component state is easiest to own and dispose. Lift state to a common parent or use a composable when several components share one workflow. Avoid a global store for every field; global state increases lifetime and coupling.

    For a larger application, Pinia is the Vue team's recommended state-management library. It adds conventions, development tooling, hot module replacement and server-side rendering support (Vue — State Management). Put business actions with meaningful names in the store rather than allowing arbitrary mutation from every component.

    State that comes from a server is not automatically client-owned truth. Define refresh, invalidation, optimistic update and conflict behaviour. Do not persist access tokens or sensitive records to browser storage merely because a store plugin makes persistence convenient.

    Avoid cross-request state leakage in SSR

    In a purely client-rendered application, a module-level singleton is new for each page load. In server-side rendering, application modules can be reused across requests. If user-specific state lives in one shared singleton, one request can leak into another.

    Vue recommends creating a new application and store instance for each SSR request and providing it to that request's component tree (Vue — Server-Side Rendering). Hydration also requires the server and client to begin with compatible state and markup.

    Serialised initial state is untrusted input when it reaches the browser. Encode it safely and avoid placing secrets in HTML.

    Optimise only after profiling updates

    Vue is efficient for ordinary interfaces. If updates are slow, first inspect Vue DevTools and browser performance traces to identify which components and data structures are involved.

    Useful patterns include:

    • keep props stable so unrelated children do not update;
    • give list items stable keys based on identity;
    • virtualise lists with thousands of visible candidates;
    • avoid rendering data the user cannot see;
    • split large asynchronous components where it improves initial delivery;
    • use shallowRef() or shallowReactive() for very large, deeply nested immutable structures; and
    • replace a shallow root value rather than mutating nested fields.

    Vue's performance guide notes that deep reactivity overhead becomes relevant for large structures with very many property accesses; shallow APIs trade convenient nested mutation for root-level replacement (Vue — Performance). Do not reach for them before measurements show a problem.

    Also consider page-load architecture. Vue advises against shipping a pure client-side SPA for content-sensitive marketing pages when server-rendered or static HTML can show useful content sooner. A project can use different rendering strategies for different routes.

    Control and evidence map for Vue 3 Reactivity: State, Derived Values and Effects Without Surprises, covering Avoid cross-request state leakage in SSR, Optimise only after profiling updates, Test behaviour through the us…
    Control and evidence map: Avoid cross-request state leakage in SSR; Optimise only after profiling updates; Test behaviour through the user's interface; General-information disclaimer.

    Test behaviour through the user's interface

    Test composables and deterministic business functions in isolation, then test components through roles, labels and visible outcomes. Include loading, error, empty and stale-response cases. Browser tests should cover a focused set of complete journeys and verify keyboard and focus behaviour.

    Do not assert private ref names or exact component trees unless they are genuinely part of a supported contract. A refactor should not destroy the suite if the user-visible behaviour remains correct.

    For help designing or reviewing a Vue interface and its API integration, see Ozlin Info's web development services or contact Ozlin Info.

    Related reading: Testing web applications: a risk-based strategy that scales.


    General-information disclaimer

    This article provides general technical information only. Patterns must be validated against the actual Vue version, rendering architecture, data sensitivity and application requirements.

    AI-assistance disclosure

    AI tools assisted with source discovery, outlining and copyediting. A human reviewer must run and verify all code examples, framework statements, service claims and the publication decision before release. No maintainability or performance outcome is guaranteed.

    Practical checklist for Vue 3 Reactivity: State, Derived Values and Effects Without Surprises, covering Test behaviour through the user's interface, General-information disclaimer, AI-assistance disclosure and related r…
    Practical checklist: Test behaviour through the user's interface; General-information disclaimer; AI-assistance disclosure; Primary sources checked.

    Primary sources checked

    Source access date: 29 August 2026.