Frontend Frameworks (React.js/Vue.js)

Chapter 8
Shared State and Frontend Data Architecture

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

Shared State and Frontend Data Architecture

Introduction

Modern frontend applications are not merely collections of screens. They are distributed, event-driven systems in which many components must interpret, modify, cache, validate, and synchronize data. A product page may depend on authentication, inventory, pricing, recommendations, shopping-cart contents, and remote API status. If these concerns are managed informally, the application develops duplicated data, unpredictable updates, excessive network requests, and difficult-to-test behaviour.

Shared state and frontend data architecture provide the conceptual and technical means to manage this complexity. In React.js and Vue.js, the central problem is not simply where to store a value. It is determining who owns the value, which parts of the application may modify it, how its validity is established, how asynchronous operations are represented, and how changes propagate without unnecessary rendering.

This chapter treats state architecture as a design discipline. It compares local, shared, server, URL, and derived state; examines unidirectional data flow; evaluates React and Vue mechanisms; introduces normalized data and finite-state modelling; and develops a practical architecture for scalable applications.

Learning Objectives

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

  1. Distinguish local, shared, server, URL, form, and derived state.
  2. Explain ownership, invariants, immutability, reactivity, and unidirectional data flow.
  3. Select suitable React or Vue state mechanisms for a given problem.
  4. Design normalized client-side data models and asynchronous state machines.
  5. Analyse performance, consistency, security, accessibility, and testing implications.
  6. Propose and justify a frontend data architecture for a realistic application.

1. State as a Data-Architecture Problem

State is information that influences the behaviour or rendered output of an application over time. A component’s current input value, the authenticated user, a product list returned by an API, and whether a request is loading are all state, although they have different lifecycles and ownership rules.

A useful classification is:

State categoryTypical ownerExamplesAppropriate lifetime
Local UI stateOne componentModal visibility, active tabComponent instance
Shared client stateSeveral componentsTheme, cart draft, wizard progressApplication session
Server stateBackend service and cacheProducts, orders, permissionsAcross sessions and requests
URL stateRouter/browserSearch query, page, filterShareable navigation state
Form stateForm boundaryField values, errors, dirty statusForm interaction
Derived stateComputationFiltered products, totalsRecomputed from source state

The classification is architectural rather than absolute. A shopping cart may begin as local state, become shared state when a header badge needs it, and eventually become server state when it must persist across devices.

1.1 Ownership and the Single Source of Truth

The owner of a state value is the authority responsible for changing it and maintaining its invariants. For example, if a cart must never contain a negative quantity, the cart model—not individual product-card components—should enforce that rule.

A single source of truth means that one authoritative representation exists for each important fact. If a user’s name is stored independently in a profile component, navigation bar, and global store, those copies can diverge. Multiple views may exist, but they should be derived from one authoritative value wherever practical.

This principle does not prohibit caching. A server response may be cached in a client data layer while the backend remains authoritative. The architecture must explicitly distinguish authoritative data from a temporary or stale representation.

1.2 State, Events, and Transitions

A robust model separates state from events and transitions:

  • State describes the current situation.
  • Event describes something that happened, such as ADD_ITEM or REQUEST_SUCCEEDED.
  • Transition determines the next state from the current state and event.

Conceptually:

St+1 = T(St, Et)

Here, St is the current state, Et is an event at time t, T is a transition function, and St+1 is the resulting state. This model assumes that the transition is deterministic for a given state and event; external effects such as network calls must be handled separately or represented by explicit events.

For a cart, an event might be INCREASE_QUANTITY(productId). The transition validates the identifier and quantity, then returns a new cart state. This is more predictable than allowing arbitrary components to mutate a shared object.

2. Core Architectural Principles

2.1 Unidirectional Data Flow

In unidirectional data flow, data moves from an owner to descendants, while descendants communicate intentions upward through callbacks, events, or actions. React commonly expresses this through props and callbacks. Vue expresses it through props and emitted events.

The pattern creates a traceable path:

user action → event/action → state transition → updated state → rendered views

It reduces hidden coupling and makes debugging possible through event histories. Bidirectional binding can be convenient for forms, but unrestricted two-way mutation makes ownership and causality harder to identify. Vue’s v-model is best understood as a controlled protocol involving a value prop and an update event, not as a reason to abandon ownership rules.

2.2 Immutability and Structural Sharing

Immutability means that an existing state value is not changed in place; an update produces a new value. In JavaScript, this is a convention enforced by application design or libraries rather than a universal language guarantee.

Immutability supports referential comparison. If an object reference has not changed, a component or selector may safely conclude that the object itself has not changed. Structural sharing means copying only the changed path rather than the entire data graph.

For example, updating one user in an immutable collection may create a new users map and a new user object while reusing unchanged users. The benefit is efficient comparison and clearer temporal reasoning. The cost is allocation, implementation complexity, and the need to avoid accidental mutation of nested objects.

2.3 State Invariants

An invariant is a condition that must remain true. Examples include:

  • every cart quantity is an integer greater than zero;
  • an order identifier is unique;
  • a loading request has a corresponding completion or failure event;
  • a selected product identifier either refers to an existing product or is null.

Invariants should be enforced at state boundaries, such as reducers, store actions, composables, or domain services. Rendering components should not be responsible for repairing invalid shared state.

2.4 Derived State and Normalization

Derived state is calculated from authoritative state. Examples include a cart subtotal, a filtered product list, and whether a form is valid. Storing derived values creates a synchronization problem: both the source and the calculation can become stale.

For a cart, the subtotal is conceptually:

subtotal = ∑i=1n pi qi

Here, pi is the unit price of item i, qi is its quantity, and n is the number of distinct cart lines. The formula assumes prices are represented consistently, preferably as integer minor units such as cents, rather than binary floating-point currency values. Tax, discounts, and shipping should be modelled separately because their rules may depend on jurisdiction and business policy.

Normalization stores entities once and refers to them by identifiers. Instead of embedding the same product object in several lists, a normalized model may contain:

{
  productsById: { p1: { id: 'p1', name: 'Keyboard' } },
  searchResultIds: ['p1'],
  featuredIds: ['p1']
}

Normalization reduces inconsistent duplicate updates, although it introduces lookup complexity and requires deliberate selector design.

3. React Approaches to Shared State

3.1 Lifting State Up

The simplest React technique is to move state to the nearest common ancestor of components that need it. The parent passes values through props and passes event handlers downward.

function SearchPage() {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');

  return (
    <>
      <SearchBox query={query} onQueryChange={setQuery} />
      <CategoryFilter value={category} onChange={setCategory} />
      <Results query={query} category={category} />
    </>
  );
}

This approach is transparent and easy to test. Its limitation is prop drilling when data must pass through many components that do not otherwise use it. Prop drilling is not automatically a design failure; it becomes problematic when intermediary components are forced to understand or relay unrelated application concerns.

3.2 Context

React Context makes a value available to descendants without explicit intermediary props. It is appropriate for relatively stable cross-cutting concerns such as locale, theme, dependency injection, or the current authenticated session.

Context does not itself provide a complete state-management architecture. A context value containing frequently changing data may cause many consumers to re-render. Context also does not automatically provide normalization, asynchronous caching, action logging, or selective subscriptions.

A useful pattern is to expose a restricted interface rather than a mutable object:

const CartContext = createContext(null);

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, initialCart);
  const value = useMemo(() => ({ cart, dispatch }), [cart]);
  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

The reducer contains transitions, while the provider controls access.

3.3 Reducers and External Stores

useReducer is valuable when state transitions are numerous, related, or governed by explicit business rules. A reducer should be pure: it should not perform network requests, access the DOM, or depend on current time without receiving that information as input.

For large applications, an external store can offer action dispatching, selectors, middleware, persistence, and development tooling. The choice of library should follow requirements rather than fashion. Important evaluation criteria include:

  • subscription granularity;
  • support for asynchronous workflows;
  • serializability and debugging;
  • server-side rendering compatibility;
  • persistence and rehydration behaviour;
  • TypeScript support;
  • team familiarity and maintenance risk.

3.4 Server State in React

Remote data has properties that ordinary client state does not: latency, errors, cancellation, retries, stale responses, cache invalidation, pagination, and refetching. A dedicated server-state or data-fetching layer can manage these concerns more reliably than hand-written effects in every component.

A request state should distinguish at least:

{
  status: 'idle' | 'loading' | 'success' | 'error',
  data: null,
  error: null
}

A Boolean such as isLoading alone cannot express whether old data is being displayed during a refresh, whether a request has never run, or whether an error has occurred. More elaborate models may distinguish refreshing, fetchingNextPage, and mutating states.

4. Vue Approaches to Shared State

4.1 Reactive State, Props, and Emits

Vue’s reactivity system tracks dependencies and updates consumers when reactive values change. A component normally receives data through props and communicates outward using emitted events:

<script setup>
defineProps({ modelValue: String })
defineEmits(['update:modelValue'])
</script>

The standard v-model convention for a component combines a modelValue prop with an update:modelValue event. The child does not need to own the parent’s value; it requests an update through the event.

4.2 Composables

A composable is a reusable function that encapsulates reactive state and behaviour. It is appropriate for concerns such as window size, authentication access, validation, or a reusable data-fetching workflow.

A composable should document whether its state is per-call or shared. If reactive state is created inside the function, each caller may receive an independent instance. If state is created at module scope, callers may share it unintentionally or intentionally. This distinction matters for testing, server-side rendering, and user isolation.

4.3 Pinia and Store Design

Pinia is a common Vue store solution. A store may expose state, getters, and actions. Getters resemble derived state; actions are suitable for domain operations and asynchronous workflows. The store should expose meaningful operations such as addProduct or submitOrder, rather than allowing every component to mutate arbitrary fields.

Vue’s reactivity is convenient, but it does not eliminate architectural questions. A reactive object can still have unclear ownership, excessive scope, accidental persistence, or insecure contents. Reactivity concerns propagation; architecture concerns meaning and authority.

5. Asynchronous Data and Consistency

5.1 The Request Lifecycle

A remote operation usually has these stages:

  1. an intent is generated;
  2. a request is started;
  3. a response succeeds, fails, or is cancelled;
  4. the result is accepted or discarded;
  5. dependent views are updated.

Race conditions occur when an earlier request completes after a later request. Suppose a user searches for ca, then quickly searches for cat. If the ca response arrives last and is accepted unconditionally, the screen displays results for the wrong query.

Solutions include request identifiers, cancellation through AbortController, query-keyed caching, or accepting a response only when its parameters match the current request. The architecture should make stale-response handling explicit.

5.2 Cache and Invalidation

A cache improves latency and reduces server load, but it introduces freshness questions. A cache policy should identify:

  • the cache key;
  • the data owner;
  • the expected freshness interval;
  • invalidation triggers;
  • behaviour during offline operation;
  • conflict resolution after mutation.

Invalidation is often safer than attempting to update every cached representation manually. After changing a product, for example, the application may invalidate product-detail and product-list queries, then refetch them. Optimistic updates can improve responsiveness but require rollback logic if the server rejects the operation.

5.3 Consistency Models in the Frontend

A frontend frequently displays eventually consistent information: the UI updates immediately, while confirmation from the server arrives later. This is acceptable only when the user is informed about pending status and failure recovery is possible.

For high-value actions such as financial transfers, the interface should not imply final success merely because a local state update occurred. The server’s authoritative response and an idempotent operation design are essential.

6. Selecting the Right State Boundary

A practical decision process is:

  1. Can the value be recomputed from other state? If yes, prefer derived state.
  2. Is it meaningful only inside one component? Keep it local.
  3. Must it be shareable through a link or browser history? Consider URL state.
  4. Does it originate from or persist to a backend? Treat it as server state.
  5. Do several distant components need it during one session? Use a shared store or context/composable.
  6. Does it belong only to a form interaction? Keep it within the form boundary unless another workflow needs it.

The smallest sufficient scope is usually the safest scope. Globalizing every value increases coupling, makes testing harder, and enlarges the potential impact of an accidental update.

7. Worked Design Example: A Collaborative Course Dashboard

Consider an illustrative postgraduate course dashboard. It contains a navigation bar, course list, assignment detail, discussion panel, and notification centre. Users can switch courses, search assignments, mark notifications read, and work with intermittent connectivity.

Problem

An initial implementation stores course data in each page, duplicates notification counts, and uses component-level effects for every API request. Users observe stale counts, repeated requests, and occasional overwriting of newer search results.

Analysis

The data can be classified as follows:

DataClassificationReason
Current course identifierURL/shared clientIt should be bookmarkable and usable by several views
Course and assignment recordsServer stateBackend is authoritative
Notification read statusServer state plus optimistic client stateIt must persist, but immediate feedback is desirable
Open discussion panelLocal UI stateNo other page needs it
Filtered assignmentsDerived stateComputed from assignments and filter criteria
Search input draftLocal/form stateIt is temporary until submitted

Architecture

The application uses a route parameter for the current course, a query-cache layer for course and assignment data, and a small session store for user identity and unread-count presentation. The unread count is derived from normalized notification records when those records are available; it is not independently edited by multiple components.

A notification mutation follows this sequence:

  1. dispatch MARK_READ(notificationId);
  2. optimistically update the local record;
  3. send an authenticated request containing the notification identifier;
  4. on success, retain the update and invalidate relevant notification queries;
  5. on failure, roll back the record and present an actionable error.

Search requests are keyed by the normalized query and course identifier. An obsolete response cannot replace current results because the cache associates responses with their keys.

Outcome

The navigation bar and notification centre read a coherent data source. Course pages reuse cached data without duplicating request logic. The URL supports deep linking. Local panel state remains isolated, reducing unnecessary updates. Tests can verify store transitions, query behaviour, and component rendering independently.

Learning Points

The most important improvement was not adopting a particular library. It was classifying data according to ownership and lifecycle, then assigning each category an appropriate boundary. The architecture also separates server authority from responsive local presentation.

8. Performance and Rendering Analysis

Shared state affects performance through both computation and propagation. A component may re-render because its selected value changed, because its parent rendered, or because a broad context/store subscription notified it.

Useful strategies include:

  • subscribe to a narrow selector rather than the entire store;
  • split providers or stores by responsibility;
  • preserve object identity for unchanged data;
  • memoize expensive derived calculations when measurement justifies it;
  • virtualize very long lists;
  • debounce high-frequency input before network requests;
  • avoid putting rapidly changing values into a global provider;
  • profile before and after optimization.

Memoization is not automatically beneficial. It adds comparison and maintenance costs. The relevant question is whether the saved rendering or computation exceeds that cost under realistic workloads.

9. Testing Shared State

A layered testing strategy is appropriate:

  • Transition tests verify reducers, actions, getters, and invariants.
  • Composable/hook tests verify reusable reactive behaviour and cleanup.
  • Contract tests verify API assumptions and serialization.
  • Component tests verify user-visible states and emitted actions.
  • Integration tests verify routing, stores, data fetching, and failure paths together.
  • End-to-end tests verify critical journeys in a browser.

Tests should cover loading, empty, success, error, retry, cancellation, stale responses, permission denial, optimistic rollback, and offline recovery where relevant. Testing only the successful rendering path gives a misleading sense of reliability.

10. Security, Privacy, and Governance

A frontend store is not a security boundary. Any value delivered to a browser can potentially be inspected or modified by the user. Authorization must be enforced by the backend, even when the interface hides controls for unauthorized actions.

Avoid storing long-lived sensitive credentials or unnecessary personal data in persistent browser storage. Consider cross-site scripting, token theft, accidental logging, browser extensions, shared devices, and server-side rendering data leakage. State rehydration must be scoped to the correct user and must not expose one user’s data to another.

Data architecture also has governance implications. Applications may need retention limits, audit trails, consent handling, data minimization, and deletion workflows under applicable privacy regulations. A normalized store should not become an uncontrolled archive of personal data.

11. Contemporary Developments and Critical Evaluation

Several developments influence frontend data architecture:

  • server components and server-side data loading can reduce client bundles and move selected data work to the server;
  • streaming and partial rendering require explicit loading boundaries;
  • offline-first applications use local persistence, synchronization queues, and conflict policies;
  • real-time transport such as WebSockets requires event ordering and reconnection design;
  • TypeScript can encode action and state shapes, but types do not replace runtime validation;
  • AI-assisted coding can generate reducers and hooks, yet generated code must be reviewed for race conditions, privacy errors, mutation, and incorrect assumptions about API contracts.

No single architecture is universally superior. A small application may be harmed by a large global store, while a multi-team product may require strong conventions and centralized observability. Architectural quality is measured by clarity of ownership, correctness under failure, changeability, and suitability to the product—not by the number of libraries used.

Best-Practice Checklist

  1. Classify every important value by source, owner, lifetime, and sharing requirement.
  2. Keep state local until a genuine sharing requirement appears.
  3. Maintain one authoritative representation of each business fact.
  4. Prefer derived selectors over duplicated calculated state.
  5. Model asynchronous status explicitly.
  6. Treat server data as cached remote state, not ordinary local variables.
  7. Prevent stale responses from overwriting current data.
  8. Enforce domain invariants in reducers, stores, or services.
  9. Keep secrets and unnecessary personal data out of persistent client state.
  10. Measure rendering performance and test failure paths.

Chapter Summary

Shared state and frontend data architecture concern the ownership, lifecycle, consistency, and propagation of information in a frontend system. Local state is best for isolated interaction; shared client state supports cross-component workflows; server state requires caching and synchronization; URL state supports navigation and deep linking; and derived state should normally be computed rather than duplicated.

React provides lifting state, props, Context, reducers, external stores, and server-state tools. Vue provides props and emits, reactive references, composables, and stores such as Pinia. Across both frameworks, durable designs use unidirectional data flow, explicit transitions, immutability or disciplined mutation, normalized entities where useful, and narrow state boundaries.

Asynchronous operations require explicit loading, success, error, cancellation, stale-response, and optimistic-update policies. Performance depends on subscription granularity and measured rendering costs. Security requires recognizing that browser state is user-accessible and cannot replace backend authorization. The central architectural skill is therefore not memorizing a library API, but matching each kind of data to an appropriate owner, lifecycle, consistency model, and access interface.

Key Terms

State: Information that influences application behaviour or rendered output.

Shared state: State accessed by multiple components or features.

Local state: State owned by a single component or narrowly scoped feature.

Server state: Data whose authoritative source is a backend service.

Derived state: A value calculated from one or more authoritative values.

Single source of truth: One authoritative representation of a fact.

State owner: The boundary responsible for changing state and preserving its invariants.

Event: A description of something that happened or an intention to change state.

Transition: A rule that maps current state and an event to a new state.

Reducer: A pure function that calculates a new state from state and an action.

Unidirectional data flow: An architecture in which data follows a traceable owner-to-view path and changes follow explicit events.

Immutability: The practice of producing new state values instead of changing existing values in place.

Structural sharing: Reusing unchanged portions of an immutable data structure.

Normalization: Storing entities once and referring to them by identifiers.

Selector: A function that reads or derives a focused portion of state.

Cache invalidation: A process for marking stored remote data stale or refreshing it.

Optimistic update: Updating the interface before server confirmation, with recovery if the operation fails.

Composable: A reusable Vue function that encapsulates reactive state and behaviour.

Prop drilling: Passing data through intermediary components that do not otherwise use it.

Invariant: A condition that must remain true throughout state transitions.

Review Questions

Short Answer Questions

  1. What is the difference between server state and shared client state?
  2. Why should derived state generally not be stored independently?
  3. What role does a reducer play in a state architecture?
  4. Explain the purpose of normalization.
  5. Why is a Boolean isLoading often insufficient for asynchronous state?

Long Answer Questions

  1. Compare React Context, useReducer, external stores, Vue composables, and Pinia as mechanisms for shared state.
  2. Explain unidirectional data flow and analyse its benefits and limitations in large applications.
  3. Design a state architecture for an e-commerce cart, including local, shared, server, URL, and derived state.
  4. Discuss stale responses, caching, invalidation, and optimistic updates in frontend applications.
  5. Analyse the security, privacy, and governance risks of persistent frontend state.

Analytical / Application Questions

  1. A search page sends a request for react, followed quickly by a request for reactive. The first response arrives last. Identify the race condition and propose two technically sound solutions.
  2. A Vue application stores the same user object in a navigation store, profile component, and messaging store. Analyse the risks and redesign the data model using a single source of truth.
  3. A React dashboard re-renders every widget whenever one notification changes. Diagnose possible architectural causes and propose a measurement-led optimization plan.

Multiple Choice Questions with Answers

Question 1

Which state category is most appropriate for a filter that must be preserved when a user copies and shares a link?

A. Component-local state only

B. URL state

C. Transient animation state

D. Unpersisted reducer state

Correct Answer: B. URL state

Question 2

What is the principal purpose of a reducer?

A. To perform DOM manipulation

B. To fetch data directly from a server

C. To calculate the next state from current state and an action

D. To replace backend authorization

Correct Answer: C. To calculate the next state from current state and an action

Question 3

Which practice best represents a single source of truth?

A. Duplicating a user name in every component

B. Storing a subtotal separately from cart lines and editing both

C. Keeping one authoritative user record and deriving views from it

D. Copying server data into unrelated local states

Correct Answer: C. Keeping one authoritative user record and deriving views from it

Question 4

Why can unrestricted mutation of shared objects be problematic?

A. It always prevents JavaScript execution

B. It obscures ownership and makes change detection and debugging harder

C. It automatically encrypts data

D. It guarantees excessive network requests

Correct Answer: B. It obscures ownership and makes change detection and debugging harder

Question 5

Which React mechanism makes a value available to descendants without passing it through every intermediary component?

A. Context

B. JSX interpolation

C. CSS modules

D. The browser cache

Correct Answer: A. Context

Question 6

In Vue, the conventional component protocol for a two-way binding using v-model involves which pair?

A. class and style

B. modelValue and update:modelValue

C. key and ref

D. slot and teleport

Correct Answer: B. modelValue and update:modelValue

Question 7

What is the main architectural reason for treating API data as server state?

A. It never changes after the first request

B. It involves freshness, caching, errors, retries, and synchronization

C. It cannot be displayed in a component

D. It is always stored securely in the browser

Correct Answer: B. It involves freshness, caching, errors, retries, and synchronization

Question 8

A cart contains two lines: 3 units at 400 minor currency units and 2 units at 250 minor currency units. What is the subtotal?

A. 1,200 minor units

B. 1,450 minor units

C. 1,700 minor units

D. 2,000 minor units

Correct Answer: C. 1,700 minor units

Question 9

Which approach most directly prevents an obsolete search response from replacing current results?

A. Accept every response in arrival order

B. Associate responses with query keys or cancel obsolete requests

C. Remove all loading indicators

D. Store results in several duplicated components

Correct Answer: B. Associate responses with query keys or cancel obsolete requests

Question 10

Which statement about frontend state and security is correct?

A. A hidden button provides backend authorization

B. Client-side stores are trusted security boundaries

C. Backend authorization remains necessary because browser state can be inspected or modified

D. Persistent storage automatically encrypts all sensitive values

Correct Answer: C. Backend authorization remains necessary because browser state can be inspected or modified

Frontend Frameworks (React.js/Vue.js)
Chapter 8 of 8