Frontend Frameworks (React.js/Vue.js)

Chapter 4
Reactivity, State, Lifecycle, and Effects

Bachelors Program in Full Stack Development
Frontend Frameworks (React.js/Vue.js)   •   Bachelor

Reactivity, State, Lifecycle, and Effects

Introduction

Modern frontend applications are not static documents. They respond to user input, network responses, timers, browser events, route changes, and changing data. React and Vue both address this problem through declarative rendering: the developer describes what the interface should look like for a given state, while the framework coordinates updates when that state changes.

Although React and Vue solve related problems, their programming models differ. React primarily uses rerendering and immutable state updates. Its function components execute again when relevant state or props change, and Hooks provide access to state, effects, references, and memoization. Vue uses a fine-grained reactivity system based on reactive proxies and refs. It tracks which reactive values are read and updates affected consumers, while the Composition API organizes component logic through functions such as ref, reactive, computed, and watch.

Understanding these differences is essential for designing predictable components. Many practical defects—stale data, infinite update loops, unnecessary network requests, memory leaks, and inconsistent forms—are synchronization errors rather than syntax errors.

Learning Objectives

By the end of this chapter, you should be able to:

  1. Explain React’s rerendering and Hook model and Vue’s proxy/ref reactivity model.
  2. Distinguish local state, props, derived state, and external side effects.
  3. Implement lifecycle behavior using React Effects and Vue lifecycle hooks.
  4. Use computed values, memoization, watchers, and references appropriately.
  5. Prevent stale closures, dependency errors, infinite loops, and resource leaks.
  6. Compare equivalent solutions in React and Vue and select an appropriate approach.

1. The Reactive UI Model

A component can be viewed conceptually as a function of inputs and internal state:

UI = render(props, state)

Here, props are inputs supplied by a parent, state is data owned by the component, and render produces a description of the user interface. This expression is a conceptual model rather than a complete implementation rule. Real applications also contain effects, browser APIs, asynchronous operations, and external stores.

A reactive framework performs three related tasks:

  • Dependency identification: determining what data a component or computation depends on.
  • Change detection: determining whether relevant data has changed.
  • Update scheduling: deciding when and how the visible interface should be reconciled with the new data.

React generally detects changes through state-update APIs and compares values during rerendering. Vue records property access through its reactivity system and can notify dependent computations more selectively. Neither model eliminates the need for sound design: developers must still identify ownership, avoid accidental mutation, and control side effects.

State, props, and derived values

State is mutable data owned by a component or store. Examples include whether a menu is open, the current form input, or the selected product.

Props are inputs from a parent. A child should normally treat them as read-only and communicate requested changes through callbacks in React or emitted events/model bindings in Vue.

Derived state is a value calculated from existing state or props. For example, a filtered product list derived from a search term and a product collection should usually not be stored separately. Storing both the source and the result creates two representations that can diverge.

A useful design principle is:

Store the minimum authoritative data; calculate the rest.

This is not an absolute rule. Expensive calculations, server-cached data, or values captured at a particular historical moment may justify storage. However, duplicated state should have a clearly defined synchronization strategy.

2. React: Rerendering and Hooks

Function components and rerendering

A React function component is called to produce an element tree. When its state changes, React schedules a rerender. The component function may execute again, so local variables inside the function do not persist between renders. Persistent values must be held in state, a ref, or an external store.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(current => current + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

The updater form, setCount(current => current + 1), is especially important when the next value depends on the previous value. It avoids relying on a possibly stale value captured by a closure and composes correctly when several updates are queued.

React may batch state updates and may render more than once in development or under concurrent scheduling. Therefore, rendering code should be pure: it should calculate output without starting network requests, registering event listeners, mutating external objects, or performing other observable actions.

State identity and immutable updates

React commonly uses reference identity to determine whether values have changed. State should be replaced rather than mutated in place:

setUser(previous => ({
  ...previous,
  name: 'Amina'
}));

For arrays, use methods that create a new array:

setItems(previous => previous.filter(item => item.id !== removedId));

Directly changing an existing object can prevent React from recognizing the intended update and can make debugging difficult. Immutability here is a state-management discipline, not a claim that JavaScript objects are intrinsically immutable.

Hooks and their rules

Common React Hooks include:

  • useState: local state.
  • useReducer: state transitions expressed through actions and a reducer.
  • useEffect: synchronization with external systems after rendering.
  • useLayoutEffect: layout-related work before the browser paints; use sparingly.
  • useRef: a persistent mutable container that does not trigger rerenders.
  • useMemo: caches a calculated value when dependencies are unchanged.
  • useCallback: caches a function identity when dependencies are unchanged.

Hooks must be called at the top level of a component or custom Hook, not conditionally, inside loops, or inside ordinary nested functions. React associates Hook calls with their consistent call order across renders. Breaking that order makes the association between calls and stored data ambiguous.

Effects as synchronization

An Effect is appropriate when a component must synchronize with something outside React, such as:

  • a WebSocket or subscription,
  • a timer,
  • a browser API,
  • an imperative widget,
  • a network request whose result updates component state.
import { useEffect, useState } from 'react';

function OnlineStatus({ userId }) {
  const [online, setOnline] = useState(false);

  useEffect(() => {
    const connection = connectToStatusService(userId);
    connection.onStatus(setOnline);

    return () => connection.disconnect();
  }, [userId]);

  return <span>{online ? 'Online' : 'Offline'}</span>;
}

The dependency array states which reactive values used by the Effect can change its synchronization. When userId changes, React performs cleanup for the previous setup and then runs the new setup. On unmount, cleanup also runs.

An Effect is not a general-purpose event handler. If an operation is caused directly by a click, place it in the click handler. If a value can be calculated during rendering, do not use an Effect merely to calculate it.

Cleanup and asynchronous work

Cleanup must release resources and prevent obsolete work from affecting current state. For fetch operations, an AbortController can cancel a request:

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/products?q=${encodeURIComponent(query)}`, {
    signal: controller.signal
  })
    .then(response => response.json())
    .then(setProducts)
    .catch(error => {
      if (error.name !== 'AbortError') throw error;
    });

  return () => controller.abort();
}, [query]);

Cancellation is particularly important when a user changes the query quickly. Without it, an older response may arrive after a newer response and overwrite current results. Cancellation reduces this race, although applications may also need request identifiers or sequence checks when the underlying operation cannot truly be cancelled.

References and stale closures

A ref persists across renders but changing ref.current does not cause a rerender:

const latestValue = useRef(value);
latestValue.current = value;

Refs are suitable for DOM nodes, timer identifiers, imperative handles, and mutable values needed by event handlers. They are not a replacement for state when the interface must update.

A closure can retain values from the render in which it was created. This is a stale closure. It often appears when an Effect has an incomplete dependency list or when a delayed callback reads state captured earlier. Solutions include declaring accurate dependencies, using functional state updates, or deliberately reading a ref when the requirement is to access the latest value without triggering a render.

3. Vue: Proxies, Refs, and the Composition API

Reactive proxies

Vue’s reactive function returns a proxy that intercepts property access and mutation:

import { reactive } from 'vue';

const form = reactive({
  name: '',
  email: ''
});

form.name = 'Amina';

Vue can track which reactive properties are read by a component, computed value, or watcher. A mutation can then invalidate dependent work. The proxy is not the original object, so identity-sensitive code should consistently use the reactive proxy.

reactive is primarily intended for objects, arrays, and collections. It has practical limitations: replacing the entire reactive object can break references held by consumers, destructuring can lose reactivity, and primitive values cannot be wrapped directly in the same way.

Refs

A ref wraps a value in an object with a reactive .value property:

import { ref } from 'vue';

const count = ref(0);
count.value++;

In Vue templates, refs are commonly unwrapped automatically, so the template can use count rather than count.value. In JavaScript expressions, .value is normally required.

Refs are useful for primitives and also for values whose identity may be replaced. Template refs can point to DOM elements after mounting:

<script setup>
import { ref, onMounted } from 'vue';

const inputElement = ref(null);

onMounted(() => {
  inputElement.value?.focus();
});
</script>

<template>
  <input ref="inputElement" />
</template>

Computed values

A computed value represents derived state:

import { computed, ref } from 'vue';

const price = ref(50);
const quantity = ref(2);
const total = computed(() => price.value * quantity.value);

Computed values are cached until a tracked dependency changes. This makes them appropriate for deterministic derivations, such as totals, filtered collections, and validation summaries. A computed value should not normally perform network requests or mutate unrelated state.

React has no direct equivalent with identical semantics. useMemo can cache a calculation, but it is a performance optimization rather than a separate reactive value category. In React, the calculation can also simply be performed during rendering when it is inexpensive.

Watchers

A watcher observes reactive data and runs a callback when the observed source changes:

import { ref, watch } from 'vue';

const query = ref('');

watch(query, (newQuery, oldQuery, onCleanup) => {
  const controller = new AbortController();

  fetch(`/api/search?q=${encodeURIComponent(newQuery)}`, {
    signal: controller.signal
  });

  onCleanup(() => controller.abort());
});

Watchers are appropriate for side effects caused by reactive changes: persistence, analytics, synchronization with non-Vue libraries, or asynchronous requests. They are not the preferred mechanism for ordinary derivation; use computed for that.

Vue also provides watchEffect, which automatically tracks reactive values read during its execution. It is convenient when the dependency set is straightforward, but explicit watch is often clearer when old values, precise sources, or controlled triggering are important.

Watchers have scheduling options. A watcher can run before or after component updates, or synchronously in specialized circumstances. The correct choice depends on whether the callback must observe pre-update state, updated DOM, or immediate changes. Code should not rely on timing without documenting that requirement.

Lifecycle hooks

The Composition API provides lifecycle hooks such as:

  • onMounted: after the component has been mounted.
  • onUpdated: after a component update.
  • onUnmounted: after removal from the interface.
  • onBeforeUnmount: immediately before removal.
  • onActivated and onDeactivated: useful with cached components.
import { onMounted, onUnmounted } from 'vue';

let timer;
onMounted(() => {
  timer = setInterval(refresh, 30_000);
});

onUnmounted(() => {
  clearInterval(timer);
});

Lifecycle hooks must be registered during component setup. Like React Effects, they establish and release relationships with external systems. The exact timing differs, but the design obligation is the same: every acquired resource needs a well-defined release path.

4. Comparing React and Vue

ConcernReactVue
Basic update modelComponent rerendering after state updatesDependency-tracked reactive invalidation
Primitive stateuseStateref
Object stateState object with immutable replacementreactive proxy or object held in a ref
Derived valueCalculation during render or useMemocomputed
Side-effect responseuseEffectwatch, watchEffect, lifecycle hooks
Persistent non-rendering valueuseRefref, ordinary closure variable, or template ref depending on purpose
CleanupFunction returned from an EffectonCleanup, onUnmounted, or hook cleanup pattern
Main disciplinePure render and accurate dependenciesCorrect reactive boundaries and controlled watchers

The frameworks should not be reduced to “React is immutable” and “Vue is mutable.” React state may contain mutable objects, but recommended updates replace relevant references. Vue proxies make direct mutation reactive, but developers still need to think about ownership, replacement, and side effects. Both frameworks reward a clear separation between pure derivation and external synchronization.

5. Managing Derived State and Synchronization

Consider a shopping cart with items and a discount code. The subtotal, discount amount, and final total are derived from authoritative inputs. Storing all three values independently creates synchronization opportunities:

const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const discount = calculateDiscount(subtotal, discountCode);
const total = subtotal - discount;

In React, this calculation may occur during rendering or be wrapped in useMemo if profiling demonstrates a meaningful cost. In Vue, it is naturally expressed with computed values. An Effect or watcher is unnecessary unless the total must be sent to an external system.

A common synchronization error is the “effect chain”: one Effect sets state, which triggers another Effect, which sets more state, even though all values could have been derived in one render. Such chains increase latency, complicate dependency reasoning, and can create loops.

Controlled and uncontrolled values

A controlled input receives its displayed value from application state and reports changes through an event. This makes the state authoritative and supports validation, formatting, and coordinated updates. An uncontrolled input retains its value in the DOM, with a ref used when the application needs to read it. Controlled inputs are often preferable for complex forms; uncontrolled inputs can be efficient and suitable for simple or library-integrated forms.

The critical question is not which style is universally superior, but where the authoritative value lives and how synchronization occurs.

6. Worked Example: Search with Debouncing and Cancellation

Suppose a product search should wait briefly after typing before requesting results. The requirements are:

  1. The displayed query updates immediately.
  2. A request begins only after a quiet interval.
  3. A previous timer and request are cleaned up.
  4. Results correspond to the current query.

A React implementation can combine state and an Effect:

function ProductSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    const timer = setTimeout(() => {
      const controller = new AbortController();

      fetch(`/api/products?q=${encodeURIComponent(query)}`, {
        signal: controller.signal
      })
        .then(response => response.json())
        .then(data => setResults(data))
        .catch(error => {
          if (error.name !== 'AbortError') console.error(error);
        });

      cleanupRequest = () => controller.abort();
    }, 300);

    let cleanupRequest = () => {};
    return () => {
      clearTimeout(timer);
      cleanupRequest();
    };
  }, [query]);

  return (
    <>
      <input value={query} onChange={event => setQuery(event.target.value)} />
      <Results items={results} />
    </>
  );
}

In production code, the request cleanup arrangement should be reviewed carefully, and a data-fetching library may provide stronger caching and race-handling behavior. The conceptual sequence is the important point: each query establishes a timer and possible request; cleanup invalidates both before the next query takes ownership.

The Vue version separates the reactive query from the watcher:

const query = ref('');
const results = ref([]);

watch(query, (newQuery, _, onCleanup) => {
  const controller = new AbortController();
  const timer = setTimeout(async () => {
    try {
      const response = await fetch(
        `/api/products?q=${encodeURIComponent(newQuery)}`,
        { signal: controller.signal }
      );
      results.value = await response.json();
    } catch (error) {
      if (error.name !== 'AbortError') console.error(error);
    }
  }, 300);

  onCleanup(() => {
    clearTimeout(timer);
    controller.abort();
  });
});

The example demonstrates a general rule: debouncing is temporal control, while cancellation is ownership control. Debouncing reduces request frequency; cancellation prevents obsolete work from winning a race.

7. Applied Case: A Live Dashboard

This is an illustrative case. A university facilities dashboard displays room occupancy, allows filtering by building, and shows a live connection status. The initial implementation stores filtered rooms, summary counts, connection objects, and loading flags in several independent variables. Users report stale counts after changing buildings, duplicate WebSocket connections during development, and memory growth after navigating away.

Analysis

The filtered list and summary counts are derived from the authoritative room data and selected building. They should not be independently stored. The WebSocket is an external resource and requires setup and cleanup. A development environment may intentionally mount and clean up components more than once to expose unsafe effects; cleanup must therefore be idempotent.

React approach

React state can hold rooms and building; filtered rooms and counts can be calculated during rendering or memoized after measurement. A useEffect creates one WebSocket for the selected campus and returns a cleanup function that removes listeners and closes the socket. The dependency list includes the campus identifier used to create the connection, not unrelated derived display values.

Vue approach

Vue can hold rooms and building in refs or a reactive object. A computed value returns the filtered rooms and another computed value returns counts. A watch or lifecycle hook creates the WebSocket and registers onCleanup or onUnmounted cleanup. The connection should be recreated only when its actual source, such as campus, changes.

Outcome

After removing duplicated derived state and defining resource ownership, the dashboard displays consistent counts. Connection creation occurs only for the active campus, and navigation releases sockets and event listeners. The principal improvement is architectural rather than framework-specific: authoritative data, pure derivation, and explicit external synchronization are separated.

Learning points

  1. A value that can be calculated should not ordinarily be synchronized through another state variable.
  2. The dependency boundary should match the external resource boundary.
  3. Cleanup is required on both normal navigation and development-time remount behavior.
  4. React and Vue differ in mechanics, but both require explicit ownership of effects.

8. Common Errors and Best Practices

Missing or excessive dependencies

An incomplete React dependency list can produce stale data. An excessive list can recreate an expensive resource unnecessarily. The remedy is not to silence a linter automatically; instead, restructure the code so the Effect depends on the true synchronization inputs.

Vue watchers can have the opposite-looking problem: watching a broad reactive object may trigger more often than intended. Prefer a precise source, such as a specific ref or getter, when only one property matters.

Infinite update loops

An Effect that updates a state value included in its own dependency list can repeatedly run. A Vue watcher that mutates its watched source can create similar feedback. Prevent loops by asking whether the update is a necessary external synchronization or merely a derived calculation.

Resource leaks

Timers, subscriptions, observers, sockets, and DOM listeners must be released. Cleanup should be safe if called more than once and should not assume that a request completed successfully.

Incorrect use of memoization

Memoization has a cost and does not repair incorrect logic. Use useMemo, useCallback, or equivalent optimization only when referential stability or measured computation cost justifies it. Vue computed caching is semantically useful for derived reactive values, but a computed callback should remain deterministic.

Accessibility and user experience

Reactive updates must remain accessible. Loading indicators should be communicated appropriately, focus should not be lost after every update, and rapidly changing results should not create confusing announcements. A live region may be appropriate for important asynchronous status, but excessive announcements can overwhelm assistive-technology users.

Security and privacy

Effects that persist data, send analytics, or call APIs must respect consent, data minimization, authentication, and authorization requirements. Never place secrets in frontend state or assume that hiding a control enforces authorization. Cancellation and cleanup protect application correctness, but they do not replace server-side security.

9. Contemporary Practice

Many professional applications use data-fetching and state libraries rather than implementing every cache, retry, deduplication, and invalidation policy manually. React applications may use libraries built around server state, while Vue applications may use composables and query libraries. These tools do not eliminate the underlying concepts: developers still need to understand ownership, lifetimes, derived data, cancellation, and loading states.

Server rendering and hydration add another constraint. Code that accesses window, the DOM, or browser-only APIs must run in an appropriate client lifecycle stage. Components should also avoid nondeterministic initial output that causes hydration mismatches. React and Vue both provide ecosystem-specific mechanisms for client-only behavior, but the general principle is shared: rendering must be safe for the execution environment.

AI-assisted code generation can produce plausible but unsafe Effects, especially missing cleanup, incomplete dependencies, or duplicated state. Generated code should be reviewed against lifecycle guarantees, race conditions, accessibility requirements, and data-protection policies. The engineer remains responsible for the behavior of the shipped system.

Chapter Summary

React and Vue provide declarative approaches to changing interfaces, but their reactivity models differ. React reruns components after state updates and uses Hooks to manage state, Effects, references, and optimization. Vue tracks reactive access through proxies and refs, with computed for cached derivation, watchers for side effects, and Composition API lifecycle hooks for resource management.

State should represent authoritative information; derived values should generally be calculated. React Effects and Vue watchers are synchronization mechanisms, not substitutes for event handlers or ordinary calculations. Every subscription, timer, request, observer, or imperative integration needs cleanup. Accurate dependencies, cancellation, immutable React updates, deliberate Vue reactive boundaries, and careful treatment of stale closures prevent common synchronization defects.

The most transferable principle is to separate pure rendering and derivation from effects that interact with the outside world. Once ownership and lifetime are explicit, the apparent differences between React and Vue become manageable design choices rather than sources of mystery.

Key Terms

Reactivity: A framework’s ability to detect relevant data changes and update dependent interface work.

State: Data owned by a component or store that can change over time.

Props: Read-only inputs supplied to a component by its parent.

Derived state: A value calculated from existing state or props.

Rerender: Re-execution or reconciliation of a component after relevant data changes.

Hook: A React function that exposes component capabilities such as state or effects.

Effect: Code used to synchronize a component with an external system.

Cleanup: Work that releases resources or invalidates obsolete asynchronous operations.

Ref: A persistent reactive or mutable value container; its exact behavior differs between frameworks.

Reactive proxy: A Vue proxy that tracks property access and mutation.

Computed value: A cached reactive derivation in Vue.

Watcher: A Vue mechanism that runs a callback when a selected reactive source changes.

Composition API: Vue’s function-based API for organizing component logic.

Stale closure: A callback that retains values from an earlier render or execution context.

Dependency array: A React Effect or memoization declaration describing relevant reactive inputs.

Immutable update: Replacing changed React state references instead of mutating them in place.

Debouncing: Delaying work until a period of inactivity has elapsed.

Cancellation: Stopping or invalidating obsolete asynchronous or external work.

Controlled input: An input whose displayed value is governed by application state.

External system: A resource outside the rendering model, such as a socket, timer, DOM API, or network service.

Review Questions

Short Answer Questions

  1. What is the difference between authoritative state and derived state?
  2. Why should React rendering code be pure?
  3. What is the purpose of a cleanup function in a React Effect?
  4. How does a Vue computed value differ from a watcher?
  5. Why does changing a React ref generally not trigger a rerender?

Long Answer Questions

  1. Compare React’s useEffect model with Vue’s watch and lifecycle-hook model. Include setup, dependencies, timing, and cleanup.
  2. Explain how stale closures arise in React and describe at least two reliable solutions.
  3. Discuss the advantages and limitations of storing object state with React immutable updates versus Vue reactive proxies.
  4. Explain why duplicated derived state can cause synchronization errors, using a shopping-cart example.
  5. Describe how debouncing and cancellation work together in an asynchronous search interface.

Analytical / Application Questions

  1. A React component stores items, filteredItems, and itemCount separately. It uses one Effect to update each derived value. Analyze the risks and propose a redesign.
  2. A Vue watcher observes an entire form object and sends a persistence request on every change. Evaluate this design and propose improvements for precision, performance, and cleanup.
  3. A dashboard opens a WebSocket whenever a campus filter changes, but old sockets remain active after navigation. Identify the lifecycle and ownership errors and design a framework-independent correction.

Multiple Choice Questions with Answers

Question 1

Which React Hook is primarily intended for synchronizing a component with an external system?

A. useState

B. useEffect

C. useMemo

D. useId

Correct Answer: B. useEffect

Question 2

Which Vue feature is most appropriate for a cached value derived from reactive state?

A. computed

B. watch

C. onUnmounted

D. reactive event

Correct Answer: A. computed

Question 3

Why is the updater form such as setCount(current => current + 1) useful in React?

A. It prevents all rerenders

B. It changes a ref without state

C. It bases the update on the latest queued state

D. It runs only during component mounting

Correct Answer: C. It bases the update on the latest queued state

Question 4

What normally happens when ref.current changes in React?

A. It automatically causes a rerender

B. It causes the component to unmount

C. It resets all Hook state

D. It changes without itself causing a rerender

Correct Answer: D. It changes without itself causing a rerender

Question 5

Which operation is generally a side effect rather than a pure derivation?

A. Calculating a filtered array from props

B. Calculating a subtotal from cart items

C. Opening a WebSocket connection

D. Formatting a date for display

Correct Answer: C. Opening a WebSocket connection

Question 6

What is the principal purpose of cleanup for a timer or subscription?

A. To guarantee that rendering runs synchronously

B. To release resources and prevent obsolete work

C. To convert state into props

D. To cache every previous result

Correct Answer: B. To release resources and prevent obsolete work

Question 7

In Vue, what does reactive primarily provide for an object?

A. A proxy that tracks relevant access and mutation

B. A permanent server-side cache

C. A replacement for all lifecycle hooks

D. A nonreactive deep clone

Correct Answer: A. A proxy that tracks relevant access and mutation

Question 8

Which design most directly avoids duplicated derived state?

A. Store every display value in separate state variables

B. Update derived values through chained Effects

C. Calculate display values from authoritative inputs

D. Mutate props whenever a display value changes

Correct Answer: C. Calculate display values from authoritative inputs

Question 9

What problem does request cancellation help reduce in a rapidly changing search interface?

A. Invalid HTML syntax

B. Older responses overwriting newer results

C. Loss of CSS specificity

D. Failure to call a component function

Correct Answer: B. Older responses overwriting newer results

Question 10

What is a stale closure?

A. A browser tab with no network connection

B. A Vue proxy that has been converted to JSON

C. A callback retaining values from an earlier render

D. A computed value that has no dependencies

Correct Answer: C. A callback retaining values from an earlier render

Frontend Frameworks (React.js/Vue.js)
Chapter 4 of 14