Component-Based Architecture

Component-Based Architecture is the architectural style in which an application is constructed by composing self-contained, reusable, encapsulated units — components — each of which owns its state, its rendering, and its interface to the rest of the system. The lineage runs from the heavyweight component models of the 1990s (COM, CORBA, JavaBeans, OSGi) through the early-2010s explosion of frontend component frameworks (Backbone.js views, AngularJS directives, then React 2013, Vue 2014, Angular 2016) to the contemporary unification of component-based UIs across all major frameworks plus the emerging web-platform standard of Web Components (Custom Elements + Shadow DOM + HTML Templates). In modern practice the term is used most commonly for frontend UI architecture — React’s functional components with hooks, Vue’s single-file components, Angular’s TypeScript-decorated components, SwiftUI views, Jetpack Compose composables — but the underlying design discipline applies broadly: a component is an entity with a clear public interface (props/parameters in, events/callbacks out), private internal state, and self-contained rendering. Composition is the central organizing principle: parent components contain child components; complex UIs emerge from nesting and arrangement, not from monolithic master files. This note traces the older history of component models, the modern frontend-component era, the explicit composition principles (props down, events up; or two-way bindings), the testability and rendering model, the state management at scale, the comparison with MVVM and MVC, and the failure modes that emerged once thousand-component applications became common.

1. The Older Heritage — Component Models of the 1990s

Long before frontend frameworks made “component” a household word, software engineering had a serious tradition of component-based development (CBD). The defining text is Clemens Szyperski’s 1998 book Component Software: Beyond Object-Oriented Programming (Addison-Wesley), which gave the foundational definition: a component is “a unit of composition with contractually specified interfaces and explicit context dependencies only.” A component, per Szyperski, must be deployable and composable independently — you can build the component once, ship it, and let other parties combine it with components they didn’t write to produce working systems.

The 1990s and early 2000s saw a series of attempts to realize this vision at the system level:

COM (Component Object Model) — Microsoft’s binary component standard, introduced in 1993 and underlying ActiveX, OLE, and a generation of Windows software. COM components exposed interfaces (collections of methods) identified by GUIDs (Globally Unique Identifiers); a client could query for an interface and call its methods without knowing the implementation language. COM was binary-stable across compilers and languages. It worked well for desktop integration scenarios (embedding an Excel spreadsheet in a Word document) but the development model was complex (reference counting via IUnknown::AddRef/Release, threading apartments, monikers, MIDL interface definitions). COM’s spiritual successor, .NET, dropped much of the binary-stability machinery.

CORBA (Common Object Request Broker Architecture) — the OMG (Object Management Group) standard for cross-platform, cross-language distributed components. CORBA defined an Interface Definition Language (IDL), an Object Request Broker (ORB) that mediated calls, and naming/lifecycle services. CORBA was language-agnostic (Java, C++, Ada, Smalltalk, COBOL all had bindings) and platform-neutral. It was widely deployed in enterprise systems in the late 1990s — telecoms, finance, defense — but the runtime complexity, the IDL ceremony, and the difficulty of debugging cross-process calls limited its adoption to organizations with the resources to absorb the overhead. CORBA today is largely legacy; the network-protocol successors are SOAP (XML-RPC plus WS-* standards) and then REST and gRPC.

JavaBeans (1996) — Sun’s Java component model. A JavaBean is a Java class that follows naming conventions (getters and setters in the form getX()/setX(value)), implements java.io.Serializable, has a public no-argument constructor, and supports introspection (a tool can query its properties at runtime). JavaBeans were aimed at GUI builder tools — the IDE could examine a Bean, expose its properties for visual editing, and wire Beans together. JavaBeans informed Enterprise JavaBeans (EJB), the Java EE component model that dominated late-1990s enterprise Java despite being widely criticized for complexity.

OSGi (Open Services Gateway initiative, 1999) — a Java component model focused on dynamic deployment: a running JVM can have OSGi bundles installed, started, stopped, and uninstalled at runtime, with each bundle having its own classloader and explicit dependency declarations. OSGi underlies the Eclipse IDE (every Eclipse plugin is an OSGi bundle) and major application servers. OSGi has remained niche outside Eclipse and certain enterprise environments because the dynamic-classloader model is genuinely difficult to debug.

The lessons from this era informed all subsequent component-based architectures:

  1. Components must have interface contracts. Without explicit interfaces, components leak their internals and composition becomes impossible.
  2. Components must be deployable independently. If component A requires recompiling the world to upgrade, it isn’t really a component.
  3. Component models must address lifecycle. Construction, dependency injection, startup, shutdown, error recovery — without standard answers, every application reinvents these.
  4. Heavyweight component models tend to fail. EJB 1.x and 2.x notoriously failed in adoption because the development burden was too high; EJB 3.x (2006) walked back much of the complexity. CORBA failed for similar reasons. The successful component models — JavaBeans, simple COM, modern frontend components — kept the core simple.

2. The Modern Frontend Component Era — 2013 Onwards

The current popular meaning of “component-based architecture” comes overwhelmingly from frontend web development. The defining moment is Jordan Walke’s release of React from Facebook in May 2013, accompanying his JSConf US talk Rethinking Best Practices. React’s central thesis: build UIs out of components that take props (immutable input) and produce rendered output (originally JSX, transpiled to React.createElement calls), with state encapsulated in the component and updates flowing one-way from parent to child via props.

React was not the first frontend component framework. Backbone.js (2010) had Backbone.View. Knockout.js (2010) had components. AngularJS (2010) had directives and later component() (1.5+). Ember.js had components. But React’s combination of (a) a virtual-DOM rendering model that decoupled “what should the UI look like” from “how to update the DOM”, (b) a one-way data flow that made reasoning about state changes tractable, and (c) JSX as a declarative-but-JavaScript way to express component output, was sufficiently better than the alternatives to drive industry-wide adoption within 3–4 years.

Vue.js (Evan You, 2014) followed quickly, adopting many of React’s ideas with a more approachable syntax (single-file components combining template, script, and styles). Angular 2 (Google, 2016) was a full rewrite of AngularJS adopting a component-based model with TypeScript decorators. Svelte (2016) compiled component definitions to imperative DOM manipulation, eliminating the runtime virtual DOM. Lit (Polymer’s successor, 2018) made Web Components ergonomic. SolidJS (2019) combined React-like JSX with fine-grained reactivity.

By 2020, the frontend was unified on the component-based model. The disagreements among frameworks are about how components work (virtual DOM vs compiled output vs fine-grained reactivity; class components vs functional with hooks; one-way vs two-way binding) but the underlying architecture — UI as a tree of components, each with props, state, and rendered output — is universal.

The same model has spread to mobile and desktop:

  • SwiftUI (Apple, 2019) — Views are structs that conform to the View protocol; body returns a description of the rendered hierarchy.
  • Jetpack Compose (Android, 2021) — Composables are functions annotated @Composable that emit UI; state observation is via State<T>/MutableState<T>.
  • Flutter (Google, 2018, broader adoption 2019+) — Widgets, both stateful and stateless, compose into a tree.
  • React Native (Facebook, 2015) — React-style components rendering to native iOS/Android views.

The architectural pattern transcends the framework. A component-based application looks similar in shape across React, Vue, Angular, SwiftUI, Compose, and Flutter even though the specific syntaxes diverge.

3. The Component as a Unit — Properties, State, and Rendering

A component, in the modern frontend sense, has three load-bearing concerns:

3.1 Props (Properties / Inputs)

Props are the immutable data passed into the component by its parent. The parent says “render a <UserCard> showing this user object”; the user object is a prop. The component reads its props and renders accordingly. Props are the component’s public interface — its API.

In React (functional component):

function UserCard({ user, onEdit, isCompact }) {
  return (
    <div className={isCompact ? "card card-compact" : "card"}>
      <img src={user.avatarUrl} alt="" />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </div>
  );
}

The component receives user, onEdit (a callback), and isCompact (a flag). It does not own these; the parent owns them and passes them down.

In Vue:

<script setup>
const props = defineProps({
  user: Object,
  isCompact: { type: Boolean, default: false }
});
const emit = defineEmits(['edit']);
</script>
<template>
  <div :class="props.isCompact ? 'card card-compact' : 'card'">
    <img :src="props.user.avatarUrl" alt="">
    <h3>{{ props.user.name }}</h3>
    <p>{{ props.user.email }}</p>
    <button @click="emit('edit', props.user.id)">Edit</button>
  </div>
</template>

In Angular:

@Component({
  selector: 'app-user-card',
  template: `
    <div [class.card-compact]="isCompact" class="card">
      <img [src]="user.avatarUrl" alt="">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button (click)="edit.emit(user.id)">Edit</button>
    </div>`
})
export class UserCardComponent {
  @Input() user!: User;
  @Input() isCompact = false;
  @Output() edit = new EventEmitter<string>();
}

The Angular variant uses @Input() and @Output() decorators to mark props and events. The shape is the same: typed inputs, typed outputs, internal rendering.

3.2 State (Internal, Mutable)

State is internal data the component owns and manages. It can change over time; when it changes, the component re-renders. State that is meaningful only inside the component — UI flags like “is this dropdown open?”, form input values during typing, animation progress — belongs in the component’s local state.

In React with hooks:

function Dropdown({ options, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div className="dropdown">
      <button onClick={() => setIsOpen(!isOpen)}>{value || "Select..."}</button>
      {isOpen && (
        <ul>
          {options.map(opt => (
            <li key={opt.value} onClick={() => {
              onChange(opt.value);
              setIsOpen(false);
            }}>{opt.label}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

isOpen is local state — the parent doesn’t need to know whether the dropdown is currently expanded. The selected value is a prop — the parent owns it.

The choice of what is state and what is a prop is the central design question of component decomposition. A general guideline: if multiple components need to read the same value, lift the value up to a common ancestor and pass it down as a prop. If only one component needs it and the value is purely UI-local, keep it as state.

3.3 Rendering

The component’s output is its rendered description of UI. In React it’s JSX; in Vue, a template; in Angular, a template; in SwiftUI, a body; in Compose, the function body. The framework converts this description into actual DOM (or native UI) and updates it efficiently when state or props change.

The render function is pure with respect to the component’s props and state: given the same props and state, it produces the same output. This pureness is what enables the framework’s diffing algorithms (React’s reconciler, Vue’s virtual DOM, Compose’s composition tree) to update the UI efficiently — only the parts that changed need to be re-rendered.

4. Composition — The Organizing Principle

The architectural idea is components compose. A complex UI is built by nesting components inside components. A Page contains a Header, Body, and Footer; the Body contains a Sidebar and a MainContent; the MainContent contains a PostList; the PostList contains many PostListItems; each PostListItem contains a Title, Excerpt, and CommentCount.

This recursive composition has several useful properties:

  1. Reuse. A Button component is written once and used in dozens of places. A UserCard is reused in the user list, in the search results, in the user profile.
  2. Encapsulation. A complex sub-feature (a date picker, a rich text editor, a chart) is contained inside a component; consumers see only its props/events interface, not its internals.
  3. Local reasoning. Looking at a component’s source tells you what it does; the rest of the system is abstracted behind props.
  4. Refactoring. Replacing a sub-feature is often a matter of replacing one component; the rest of the application is unchanged.

4.1 Props Down, Events Up (React, Vue’s defineEmits, Angular’s @Output)

The dominant data-flow pattern: parent passes data down to children via props; children communicate up to parents via events (callbacks).

Parent
  data -> child1
  data -> child2 (child2 might call onSomethingHappened)
        ^ event

This is unidirectional data flow. State lives at one place in the tree (often a parent); reads flow down via props; writes flow up via events; the parent re-renders the affected subtree.

The unidirectional discipline avoids a class of bugs from earlier UI architectures where any component could mutate any other component’s data, producing untraceable cascades of updates. With unidirectional flow, you can always answer “where does this data come from?” by following the props upward, and “what causes this data to change?” by finding the event handler that mutates the state.

4.2 Two-Way Binding (Angular, Vue’s v-model, Svelte’s bind:)

Some frameworks support an alternative pattern: bind a parent’s state directly to a child’s input. The child reading the bound prop sees the parent’s value; the child mutating the bound prop mutates the parent’s value. This is two-way binding, and it sugars over the props-down/events-up pattern.

Vue’s v-model is canonical:

<input v-model="username" />

is equivalent to:

<input :value="username" @input="username = $event.target.value" />

Two-way binding is more concise but conflates the two directions. React deliberately avoided it (controlled inputs require explicit value and onChange) on the grounds that two-way binding makes data flow harder to trace at scale.

4.3 Slots / Children / Composition Slots

Sometimes a parent wants to pass a chunk of UI into a child rather than just data. This is composition in a different sense: the child renders a structure with a hole where the parent’s UI goes.

In React, this is props.children:

function Card({ children }) {
  return <div className="card">{children}</div>;
}
// usage:
<Card>
  <h2>Hello</h2>
  <p>World</p>
</Card>

In Vue, <slot/>:

<template>
  <div class="card">
    <slot />
  </div>
</template>

In Angular, <ng-content/>. In Web Components, the <slot> element.

This pattern — content projection — is essential for layout and container components (modals, cards, tabs, navigation drawers). The container provides chrome and behavior; the consumer provides content.

5. Worked Example — A TodoList Application

To anchor the abstract pattern, here is a complete TodoList in React with hooks. The structure illustrates composition, state lifting, and unidirectional data flow.

5.1 The Component Tree

TodoApp
+- TodoInput (new-todo entry box)
+- TodoFilter (All / Active / Completed)
+- TodoList
|   +- TodoItem (rendered for each todo)
+- TodoStats (counts of remaining/completed)

5.2 The Code

// TodoApp owns the state. State is "lifted" here because multiple children read/write it.
function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed'
 
  const addTodo = (text) => {
    setTodos([...todos, { id: crypto.randomUUID(), text, done: false }]);
  };
 
  const toggleTodo = (id) => {
    setTodos(todos.map(t => t.id === id ? { ...t, done: !t.done } : t));
  };
 
  const deleteTodo = (id) => {
    setTodos(todos.filter(t => t.id !== id));
  };
 
  const visibleTodos = todos.filter(t =>
    filter === 'all' ? true : filter === 'active' ? !t.done : t.done
  );
 
  return (
    <div className="todo-app">
      <h1>Todos</h1>
      <TodoInput onAdd={addTodo} />
      <TodoFilter current={filter} onChange={setFilter} />
      <TodoList todos={visibleTodos} onToggle={toggleTodo} onDelete={deleteTodo} />
      <TodoStats todos={todos} />
    </div>
  );
}
 
function TodoInput({ onAdd }) {
  const [text, setText] = useState('');
  const submit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text.trim());
      setText('');
    }
  };
  return (
    <form onSubmit={submit}>
      <input value={text} onChange={e => setText(e.target.value)}
             placeholder="What needs doing?" />
    </form>
  );
}
 
function TodoFilter({ current, onChange }) {
  return (
    <div className="filter">
      {['all', 'active', 'completed'].map(f => (
        <button key={f}
                disabled={f === current}
                onClick={() => onChange(f)}>{f}</button>
      ))}
    </div>
  );
}
 
function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul className="todo-list">
      {todos.map(t => (
        <TodoItem key={t.id} todo={t} onToggle={onToggle} onDelete={onDelete} />
      ))}
    </ul>
  );
}
 
function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={todo.done ? 'done' : ''}>
      <input type="checkbox" checked={todo.done}
             onChange={() => onToggle(todo.id)} />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
}
 
function TodoStats({ todos }) {
  const remaining = todos.filter(t => !t.done).length;
  return (
    <p className="stats">{remaining} of {todos.length} remaining</p>
  );
}

5.3 What This Demonstrates

  • State lifting. The todos and filter state live in TodoApp because multiple children need to read or modify them. TodoInput has its own local text state because no other component needs it.
  • Props down. The list of visible todos is passed down to TodoList, then to each TodoItem. The current filter goes down to TodoFilter. The total todos array goes down to TodoStats.
  • Events up. TodoInput calls onAdd to bubble new todos up. TodoFilter calls onChange. TodoItem calls onToggle/onDelete. The parent handles all mutations.
  • Composition. Each component does one thing: input handling, filtering, listing, stats. They compose into a working app.
  • Re-render efficiency. When a todo is toggled, React re-renders TodoApp (state changed), then re-renders the children with new props; React’s diffing reuses unchanged DOM nodes and only mutates what changed.

5.4 What Becomes Awkward

When this app grows, the pattern starts to strain:

  • Prop drilling — if a deeply nested grandchild needs the todos array, it has to be passed through every intermediate component. With ten levels of nesting, prop-drilling is tedious. The fix is React’s Context API or a state management library (see §7).
  • Cross-cutting concerns — adding sorting, undo/redo, persistence, analytics — all require touching the parent and threading data down. The component tree starts to feel like the wrong abstraction for shared logic.
  • Stale-closure bugs — a callback captured in a useEffect sees the value of state at render time, not at call time. This is a notorious source of bugs in functional React.

These are the maturity-curve issues that drove the rise of state management libraries.

6. State Management at Scale

For applications beyond a few dozen components, local-state-plus-prop-drilling collapses. The community has produced several state management libraries:

6.1 Redux (Dan Abramov & Andrew Clark, 2015)

Redux popularized the Flux pattern (originally from Facebook 2014) with three principles: a single source of truth (one store), state is read-only (mutate via dispatched actions), changes are made with pure functions (reducers). The Redux store is global; any component can subscribe to slices of it via connect (HOC) or useSelector (hook). Actions are dispatched, reducers compute the new state, the store notifies subscribers, components re-render.

Redux’s strength: predictable, debuggable, time-travelable (the Redux DevTools can record and replay actions). Its weakness: enormous boilerplate (action creators, action types, reducers, selectors). The Redux Toolkit (2019) reduced the boilerplate substantially.

6.2 MobX (Michel Weststrate, 2015)

MobX takes the opposite approach: observable state with automatic tracking of which components depend on which observables. Mutating an observable triggers re-renders of every component that read it during its last render. Less boilerplate than Redux, but less predictable — the implicit reactivity can produce surprising behavior.

6.3 Vuex / Pinia (Vue ecosystem)

Vuex (2015) was Vue’s official state library, modeled on Redux but more Vue-flavored. Pinia (2019, official since Vue 3) replaced Vuex with a simpler, TypeScript-friendly API. Pinia stores are reactive objects; components access them directly without prop-drilling.

6.4 NgRx (Angular)

Angular’s Redux port. Reactive Extensions (RxJS)-heavy, with selectors as observables. Standard in large Angular applications.

6.5 Zustand, Jotai, Recoil (modern React)

Lighter alternatives to Redux: Zustand is a 1KB store with hooks; Jotai is atom-based; Recoil (Facebook, 2020) introduced atoms-and-selectors with experimental concurrent support. The current React community trend is toward smaller, more focused state libraries plus React Server Components for cross-cutting data.

6.6 Server State Libraries — TanStack Query / SWR

A different angle: most state in real applications is server state (data fetched from an API) and has different concerns from UI state — caching, invalidation, refetching, optimistic updates. TanStack Query (formerly React Query, by Tanner Linsley, 2019) and SWR (by Vercel, 2019) are libraries dedicated to server-state management. They have largely replaced Redux for fetching/caching scenarios, leaving Redux/Zustand for genuine client state.

The state management landscape is fragmented. The choice depends on application size, team familiarity, and the kind of state being managed (UI flags vs cached server data vs persisted user state).

7. Web Components — The Cross-Framework Standard

Parallel to framework-specific component models, the web platform has its own component standard: Web Components. The standard has three pieces:

  • Custom Elements — define your own HTML tags via customElements.define('my-element', MyElementClass). The class extends HTMLElement and gets lifecycle callbacks (connectedCallback, disconnectedCallback, attributeChangedCallback).
  • Shadow DOM — encapsulated DOM tree attached to an element, with style isolation (CSS in the shadow tree doesn’t leak out, and the page’s CSS doesn’t leak in).
  • HTML Templates<template> element holding inert HTML that can be cloned at runtime.

A simple Web Component built using safe DOM APIs (no string-based HTML insertion):

class CounterElement extends HTMLElement {
  constructor() {
    super();
    this.count = 0;
    const shadow = this.attachShadow({ mode: 'open' });
 
    const style = document.createElement('style');
    style.textContent = 'button { padding: 8px; }';
 
    this.span = document.createElement('span');
    this.span.textContent = String(this.count);
 
    const button = document.createElement('button');
    button.textContent = '+';
    button.addEventListener('click', () => {
      this.count++;
      this.span.textContent = String(this.count);
    });
 
    shadow.append(style, this.span, button);
  }
}
customElements.define('my-counter', CounterElement);

Used as <my-counter></my-counter> in any HTML, regardless of the surrounding framework. The example uses document.createElement plus textContent rather than string-based DOM insertion — production-quality Web Components avoid string-based DOM mutation precisely to prevent cross-site-scripting injection vectors. Libraries like Lit add ergonomic templating that compiles to safe DOM operations.

Web Components are the cross-framework lingua franca. A design system implemented as Web Components can be consumed by React, Vue, Angular, Svelte, or vanilla HTML. Companies with multiple frontend teams using different frameworks (Salesforce’s Lightning Web Components, GitHub’s components, Microsoft’s FAST) have invested heavily in Web Components for this reason.

The framework community has been ambivalent. React 18 has improved Web Components interop but still has rough edges; Lit (Polymer’s successor) is the most ergonomic library for building Web Components. The web-platform standard is solid but the ecosystem (component libraries, devtools, community knowledge) lags the framework-specific ecosystems.

8. Comparison with MVVM

MVVM and Component-Based Architecture overlap heavily but are not identical:

  • MVVM separates a per-screen ViewModel containing all state and logic; the View binds to it. The ViewModel is a distinct class.
  • Component-Based distributes state across the component tree; each component owns its slice. There is no single per-screen ViewModel by default; there can be (a screen-level container component that holds the screen’s state) but the architecture doesn’t enforce it.
  • MVVM relies on data binding as a framework primitive. Components rely on rendering as a function of state — same conceptual behavior, different mechanism.

In practice the two converge: a large React application with a screen-level “container” component holding state and dumb child “presentational” components is structurally MVVM with the container as the ViewModel. A SwiftUI screen with a single @StateObject ViewModel and a tree of subviews binding to its properties is MVVM and Component-Based simultaneously.

The interview-relevant nuance: Component-Based emphasizes composition; MVVM emphasizes separation of concerns. Both are valid architectural lenses on the same code.

9. Comparison with MVC

The shift from MVC to Component-Based Architecture is generational. Server-side MVC (Rails-style) is still alive (Hotwire, HTMX, LiveView are reviving server-rendering) but client-side MVC has been displaced by component models. The reasons:

  • MVC’s controller was a monolithic dispatcher per page. A component-based UI is many small functions, not one big controller. Easier to reason about each piece in isolation.
  • MVC’s view-model coupling at the schema level was brittle. Components have explicit prop interfaces; schema changes are local.
  • MVC’s request/response model didn’t fit interactive single-page applications. Modern web UIs are stateful; components naturally express state.

Server-side MVC still wins for content-heavy, low-interactivity applications (blogs, marketing sites, simple CRUD admin tools); component-based (with optional server-rendering) wins for interactive applications.

10. Pitfalls

10.1 Prop Drilling

The classic component-based failure mode: data needed deep in the tree has to be threaded through every intermediate component as a prop. With 8 layers of nesting, this is painful and brittle (renaming a prop requires updating every layer). The fix is React’s Context API, Vue’s provide/inject, or a state library. But over-using Context can hurt performance (every consumer re-renders when the context value changes). The right tool varies.

10.2 Over-Fragmenting Components

The opposite failure: every visual element becomes a component. A 1,000-component application where every label, span, and div is its own component is hard to navigate, hard to type-check, and incurs rendering overhead. Components should be meaningful units — a logical region with cohesive concerns — not just every visual primitive. The judgment is “would another part of the application want to reuse this?” and “does this hide significant complexity?“

10.3 The useEffect Mental Model

React’s useEffect is the most-misunderstood hook. It runs after rendering and (with a dependency array) re-runs when dependencies change. Common bugs: forgetting the dependency array (effect runs on every render); listing wrong dependencies (effect goes stale); using effects for what should be derived state (should be useMemo); using effects for event handlers (should be event handlers). The React team’s official guidance “You Might Not Need an Effect” (https://react.dev/learn/you-might-not-need-an-effect) is a litany of effect-misuse patterns. For interview purposes: be ready to identify effect anti-patterns.

10.4 Re-Render Cascades

In React, a state update causes the component plus all descendants to re-render. With deeply nested trees and many state updates, this can hurt performance. Mitigations: React.memo to skip re-rendering when props are referentially equal; useMemo for expensive derived computations; useCallback for stable function references. These are correctness-neutral but performance-critical optimizations. Inexperienced engineers either ignore them (perf problems) or apply them everywhere (premature optimization that obscures the code).

10.5 Stale-Closure Bugs

A callback captured in useEffect or setTimeout sees the values of variables at the time of capture, not at the time of call. The fix is useRef + current for mutable references that don’t trigger re-renders, or carefully listing dependencies. Stale closures are unique to functional React and Vue’s composition API; class-component React didn’t have this issue.

10.6 Component Library Lock-In

Choosing a UI component library (Material-UI, Ant Design, Chakra, Mantine, Radix) couples the application to that library’s design language and update cadence. Migrating later is a multi-quarter effort. Worth weighing this in the initial library choice; “we can swap libraries later” is rarely true.

10.7 Mixing Server and Client Components (Modern React)

React Server Components (RSC, 2020 RFC, gradually rolling out 2022–2024) introduce a distinction between server-rendered and client-rendered components, with strict rules on what each can do. The mental model is genuinely new and the tooling/error messages are not yet mature; debugging RSC errors in 2026 is still rough. The architecture is promising (smaller client bundles, better SEO, built-in data fetching) but the migration cost is real.

10.8 The State Management Religious War

Picking a state library is contentious. Redux loyalists, MobX loyalists, “just use useState/Context” proponents, and Zustand fans all believe their choice is correct. The truth: each fits some applications better than others, and most are good enough for most apps. Spending a week debating the choice for a 6-month project is wasted; spending five minutes on a project lasting decades is worth it.

10.9 Tree Reconciliation Surprises

React’s reconciler uses keys to identify list items. Wrong keys (using array indices when items can be reordered) produce subtle bugs: state of one item sticks to another after reordering. The rule “use stable keys” is simple but easy to violate when keys are not obvious.

10.10 Renderer Performance with Many Components

A list of 10,000 items rendered as 10,000 components causes performance problems regardless of framework. Solutions: virtualization (react-virtualized, react-window) — render only the visible items; pagination; infinite scroll with limited DOM. Forgetting virtualization on large lists is a common performance bug.

11. Real-World Deployments

  • Facebook / Instagram / WhatsApp Web — React, where it was born. The Meta-engineering blog has frequent posts on internal React patterns (Suspense, RSC, Relay).
  • Netflix — React for the web app; React Native for some mobile flows.
  • Airbnb — Open-sourced many React patterns (Enzyme test library, code-style guide).
  • Uber — historically AngularJS, then a Fusion.js/React migration; mobile is mostly native.
  • Google — Angular for many internal tools and Google Cloud Console; React for some products; Polymer/Lit for some surfaces. Web Components for the design-system layer.
  • Microsoft — React for VS Code’s renderer (Electron), Office.com, GitHub (acquired). FAST + Lit for cross-product Web Components.
  • Apple — SwiftUI for iOS/macOS apps. Within Apple’s web properties, mixed.
  • Alibaba / Baidu / Tencent — Vue is huge in China; many large applications use it.

The breadth of adoption is what makes Component-Based Architecture the dominant frontend paradigm of the 2020s. There is essentially no major frontend application built greenfield in 2026 that is not component-based.

12. Diagram — Component Tree and Data Flow

flowchart TB
    subgraph Tree["Component Tree"]
        APP[TodoApp<br/>state: todos, filter]
        APP --> INP[TodoInput<br/>state: text]
        APP --> FLT[TodoFilter]
        APP --> LIST[TodoList]
        APP --> STATS[TodoStats]
        LIST --> ITEM1[TodoItem]
        LIST --> ITEM2[TodoItem]
        LIST --> ITEM3[TodoItem]
    end
    APP -->|"props: visibleTodos,<br/>onToggle, onDelete"| LIST
    LIST -->|"props: todo,<br/>onToggle, onDelete"| ITEM1
    ITEM1 -.->|"onToggle(id) event"| LIST
    LIST -.->|"forwarded event"| APP
    INP -.->|"onAdd(text) event"| APP
    FLT -.->|"onChange(filter) event"| APP

What this diagram shows. The TodoList component tree from §5 with its data flow. Solid arrows are props going down (state-derived data passed from parent to children). Dashed arrows are events going up (callbacks bubbling user actions back to the parent that owns the relevant state). State (todos, filter) lives in TodoApp; descendants read derived data via props and signal mutations via events. Critical observations: (1) State is centralized in the lowest common ancestor that needs it; the principle “lift state up” is the central decomposition discipline. (2) No descendant directly mutates TodoApp’s state; mutations always go through events that reach the state owner. (3) TodoInput has its own local text state because nothing else needs it — components own their UI-internal state. (4) The diagram is acyclic for data flow (down) and acyclic for events (up); the loop closes only at the state owner. This unidirectionality is what makes large component trees tractable: you can always answer “what makes this re-render?” by tracing props back to their source state. In a multi-direction-binding architecture (older AngularJS), this guarantee did not hold and subtle re-render storms were common.

13. Open Questions

  • How does the rise of React Server Components change the architectural balance between component-based UIs and traditional server-rendered MVC?
  • Is the framework fragmentation (React vs Vue vs Angular vs Svelte vs Solid) likely to converge, or remain a competitive ecosystem?
  • When should an application choose Web Components over a framework-specific component model? The cross-framework portability is appealing but ecosystem maturity matters.
  • How should microfrontend architectures balance team autonomy (each team uses its preferred framework) against integration cost (bundle size, runtime overhead, design consistency)?
  • Are state management libraries (Redux, Zustand, etc.) inherently necessary at scale, or does sufficient discipline with built-in tools (Context, custom hooks) suffice?
  • How will declarative-UI mobile frameworks (SwiftUI, Compose) influence web-frontend architecture as patterns transfer back?

14. See Also