MVC vs Component Architecture
Two fundamentally different ways to organize frontend code. MVC splits by responsibility (Model, View, Controller). Component architecture splits by feature (self-contained UI units). Understanding both — and why the industry shifted — is essential for system design interviews.
Table of Contents
Overview
MVC (Model-View-Controller) and Component Architecture are two paradigms for organizing frontend code. MVC separates an application into three layers by responsibility — data, presentation, and control flow. Component architecture splits the UI into self-contained, reusable units where each component owns its markup, logic, and styling.
MVC dominated web development for over a decade (Backbone.js, Angular.js, Rails-rendered views). The industry shifted to component architecture with React, Vue, and Angular because modern UIs are too interactive and state-heavy for clean layer separation. Understanding both patterns — and why the shift happened — is a common interview topic.
The core question is: do you organize code by technical role(all models together, all views together) or by feature (everything for a search bar in one place)? The answer shapes how teams scale, how code is reused, and how easy it is to reason about changes.
Why this matters in interviews
"How would you architect a large frontend application?" is one of the most common system design questions. Interviewers want to hear you compare paradigms, explain trade-offs, and justify your choice — not just say "I'd use React."
The Problem: Organizing Frontend Code
As applications grow, unstructured code becomes unmaintainable. The question every team faces: how do you organize hundreds of files so that changes are safe, features are isolated, and new developers can onboard quickly?
Small app (5 files): Everything in one place. No architecture needed. Medium app (50 files): Need some structure. Where does the API call go? Where does the form validation live? Where is state managed? Large app (500+ files): Architecture is critical. ├── Can a developer find the code for a feature? ├── Can they change it without breaking other features? ├── Can two teams work on different features simultaneously? └── Can components be reused across the app? Two approaches to solve this: MVC: Organize by ROLE Component: Organize by FEATURE ├── models/ ├── SearchBar/ │ ├── user.js │ ├── SearchBar.tsx │ ├── product.js │ ├── useSearch.ts │ └── cart.js │ └── SearchBar.css ├── views/ ├── ProductCard/ │ ├── userView.js │ ├── ProductCard.tsx │ ├── productView.js │ ├── useProduct.ts │ └── cartView.js │ └── ProductCard.css └── controllers/ └── Cart/ ├── userController.js ├── Cart.tsx ├── productController.js ├── useCart.ts └── cartController.js └── Cart.css
Scalability
As the app grows, can you add features without the codebase becoming a tangled mess? Architecture determines whether 10 developers can work in parallel or step on each other.
Maintainability
When a bug is reported in the search feature, how many files do you need to touch? Good architecture means changes are localized, not scattered across layers.
Separation of Concerns
Each piece of code should have one job. The question is: do you separate by technical concern (data vs UI) or by domain concern (search vs cart)?
The key insight
MVC separates concerns by technical layer — all data logic in one place, all UI in another. Component architecture separates concerns by feature — everything related to a search bar lives together. Both achieve separation of concerns, but at different boundaries.
What is MVC?
MVC is a design pattern that divides an application into three interconnected layers. Each layer has a single responsibility, and they communicate through well-defined interfaces.
┌──────────────────────────────────────────────────┐ │ MVC Pattern │ ├──────────────────────────────────────────────────┤ │ │ │ ┌────────────┐ ┌──────────────┐ │ │ │ MODEL │ │ VIEW │ │ │ │ │ │ │ │ │ │ • Data │◄───│ • UI/HTML │ │ │ │ • State │ │ • Templates │ │ │ │ • Business │ │ • Rendering │ │ │ │ logic │───►│ • Display │ │ │ └─────▲──────┘ └──────▲───────┘ │ │ │ │ │ │ │ ┌──────────────┴──────┐ │ │ │ │ CONTROLLER │ │ │ └───│ │ │ │ │ • Handles user input│ │ │ │ • Updates model │ │ │ │ • Selects view │ │ │ │ • Orchestrates flow │ │ │ └─────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ Model: The data layer. Manages state, business rules, and data validation. Knows nothing about the UI. View: The presentation layer. Renders data to the screen. Knows nothing about business logic. Controller: The glue. Receives user input, calls the model to update data, and tells the view to re-render.
// ── MODEL ───────────────────────────────────── // Manages data and business logic class TodoModel { constructor() { this.todos = []; this.listeners = []; } addTodo(text) { this.todos.push({ id: Date.now(), text, done: false }); this.notify(); // Tell listeners (views) that data changed } toggleTodo(id) { const todo = this.todos.find(t => t.id === id); if (todo) todo.done = !todo.done; this.notify(); } notify() { this.listeners.forEach(fn => fn(this.todos)); } } // ── VIEW ────────────────────────────────────── // Renders UI, knows nothing about business logic class TodoView { constructor(container) { this.container = container; } render(todos) { this.container.innerHTML = todos .map(t => `<li class="${t.done ? 'done' : ''}">${t.text}</li>`) .join(""); } onItemClick(handler) { this.container.addEventListener("click", e => { handler(e.target.dataset.id); }); } } // ── CONTROLLER ──────────────────────────────── // Connects model and view, handles user actions class TodoController { constructor(model, view) { this.model = model; this.view = view; // Model changes → update view this.model.listeners.push(todos => this.view.render(todos)); // User clicks → update model this.view.onItemClick(id => this.model.toggleTodo(id)); } addTodo(text) { this.model.addTodo(text); } }
MVC origins
MVC was invented in 1979 for Smalltalk desktop applications. It became the dominant pattern for server-side web frameworks (Rails, Django, Spring MVC, ASP.NET MVC) where the server renders HTML. Frontend MVC (Backbone.js, Ember.js) adapted the pattern for client-side JavaScript.
MVC Flow (Deep Dive)
Understanding the data flow in MVC is critical. The pattern enforces a specific communication path between layers — but in practice, frontend MVC often deviates from the ideal.
User interacts with the View
The user clicks a button, submits a form, or types in an input. The View captures this event but does NOT handle it — it delegates to the Controller.
Controller receives the input
The Controller interprets the user action. It decides what needs to happen: validate input, call an API, update data. The Controller contains the application's control flow logic.
Controller updates the Model
The Controller calls methods on the Model to change data. For example: model.addTodo(text) or model.toggleComplete(id). The Controller never manipulates the DOM directly.
Model notifies the View
After data changes, the Model emits an event or calls registered listeners. The View is subscribed to these changes and knows it needs to re-render.
View re-renders with new data
The View reads the updated data from the Model and re-renders the UI. The cycle is complete. The View never modifies data — it only reads and displays.
Classical MVC Flow: User Action ──► Controller ──► Model ──► View ──► User Sees Update │ │ └─────────────────────────────────────┘ (cycle repeats) Frontend MVC Reality (the messy version): ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ View 1 │────►│ Controller │────►│ Model A │ │ View 2 │────►│ Controller │────►│ Model B │ │ View 3 │◄────│ Controller │◄────│ Model A │ └──────────┘ └──────────────┘ └──────────┘ │ │ │ │ Views can listen to multiple models│ │ Controllers can update multiple │ │ models and views │ └───────────────────────────────────────┘ In complex UIs, the clean one-way flow breaks down. Multiple views depend on multiple models. Controllers become "god objects" that know everything. This is why Facebook moved away from MVC to Flux → Redux → React.
The Facebook story
Facebook famously described their MVC scaling problem at JSConf 2014. With hundreds of views and models, data flow became unpredictable — a change in one model could cascade through multiple views and controllers in unexpected ways. This led to Flux (unidirectional data flow) and eventually React's component model.
Limitations of MVC in Frontend
MVC works well for server-rendered applications where the request-response cycle is clear. But modern frontend applications are long-lived, highly interactive, and state-heavy — and MVC struggles with this.
Problem 1: Bidirectional Data Flow ────────────────────────────────── Model A changes → View 1 updates → triggers Controller → updates Model B → View 2 updates → triggers Controller → updates Model A again → infinite loop? race condition? In complex UIs, cascading updates are hard to trace and debug. Problem 2: Controller Bloat ────────────────────────────────── // A "god controller" that knows too much: class DashboardController { handleSearch() { ... } handleFilter() { ... } handleSort() { ... } handlePagination() { ... } handleUserProfile() { ... } handleNotifications() { ... } handleSettings() { ... } // 500+ lines of orchestration logic } Problem 3: Scattered Feature Code ────────────────────────────────── To understand the "search" feature, you must read: models/searchModel.js (data) views/searchView.js (UI) controllers/searchCtrl.js (logic) templates/search.html (markup) styles/search.css (styling) 5 files in 5 different directories for one feature. Problem 4: Difficult Reuse ────────────────────────────────── Want to reuse a "date picker" in another project? You need: the view, its model, its controller, its template, its styles, and all their dependencies. Components solve this — one import, everything included.
Tight Coupling Between Layers
Controllers often reference specific views and models directly. Changing one layer frequently requires changes in the other two. The 'separation' becomes theoretical.
State Synchronization
When multiple views depend on the same model, keeping them in sync is error-prone. A change in one view can trigger unexpected updates in others through shared models.
Testing Complexity
Testing a controller requires mocking both the model and the view. Testing a view requires a model with the right state. The layers are independent in theory but coupled in practice.
Team Scaling
In MVC, the 'search team' must touch files in models/, views/, and controllers/. Two teams working on different features often edit the same controller file, causing merge conflicts.
MVC isn't bad — it's mismatched
MVC is excellent for server-side applications (Rails, Django) where each request is independent and the controller handles one action at a time. The pattern struggles with long-lived, stateful frontend applications where dozens of UI elements react to shared state simultaneously.
What is Component Architecture?
Component architecture breaks the UI into self-contained, reusable units. Each component encapsulates its own markup, logic, styling, and state. The application is a tree of components, from a root App down to individual buttons and inputs.
┌─────────────────────────────────────────────────┐ │ Component Architecture │ ├─────────────────────────────────────────────────┤ │ │ │ ┌─────────────────────────────────────────┐ │ │ │ App │ │ │ │ ┌──────────┐ ┌────────────────────┐ │ │ │ │ │ Header │ │ Dashboard │ │ │ │ │ │ ┌──────┐ │ │ ┌──────┐ ┌─────┐ │ │ │ │ │ │ │ Logo │ │ │ │Search│ │Stats│ │ │ │ │ │ │ │ Nav │ │ │ │Bar │ │Card │ │ │ │ │ │ │ └──────┘ │ │ └──────┘ └─────┘ │ │ │ │ │ └──────────┘ │ ┌──────────────┐ │ │ │ │ │ │ │ TodoList │ │ │ │ │ │ │ │ ┌──────────┐ │ │ │ │ │ │ │ │ │ TodoItem │ │ │ │ │ │ │ │ │ │ TodoItem │ │ │ │ │ │ │ │ │ │ TodoItem │ │ │ │ │ │ │ │ │ └──────────┘ │ │ │ │ │ │ │ └──────────────┘ │ │ │ │ │ └────────────────────┘ │ │ │ └─────────────────────────────────────────┘ │ │ │ │ Each box is a COMPONENT that owns: │ │ • Its own markup (JSX/template) │ │ • Its own logic (event handlers, hooks) │ │ • Its own state (useState, local data) │ │ • Its own styles (CSS modules, styled-comp) │ └─────────────────────────────────────────────────┘
// Everything for TodoItem lives in ONE component function TodoItem({ todo, onToggle }) { return ( <li className={todo.done ? "done" : ""} onClick={() => onToggle(todo.id)} > {todo.text} </li> ); } // Parent component manages state and passes props function TodoList() { const [todos, setTodos] = useState([]); const addTodo = (text) => { setTodos(prev => [...prev, { id: Date.now(), text, done: false }]); }; const toggleTodo = (id) => { setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t) ); }; return ( <div> <TodoInput onAdd={addTodo} /> <ul> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={toggleTodo} /> ))} </ul> </div> ); } // Compare with MVC: no separate model, view, controller files. // State, logic, and UI live together in the component. // Reuse TodoItem anywhere — just import and pass props.
Encapsulation
Each component is a black box. It receives props, manages internal state, and renders UI. Other components don't need to know how it works internally.
Reusability
A well-designed component can be used anywhere — different pages, different projects, different teams. Import it, pass props, done. No model/controller dependencies.
Composition
Complex UIs are built by composing simple components. A Dashboard is a Header + Sidebar + Content. Content is a SearchBar + DataTable + Pagination. Each piece is independent.
The mental model shift
In MVC, you think: "I need a model for user data, a view to display it, and a controller to connect them." In component architecture, you think: "I need a UserProfile component that fetches and displays user data." The component is the feature.
Component-Based Flow
In component architecture, data flows in one direction: from parent to child via props. State changes trigger re-renders. Events flow upward via callbacks. This unidirectional flow makes the application predictable and debuggable.
State lives in a component
A component declares state using useState (React), ref (Vue), or a state management library. The state is the single source of truth for that piece of data.
Props flow downward
Parent components pass data and callbacks to children via props. Children receive props as read-only inputs — they never modify props directly.
User interacts with a child component
The user clicks a button or types in an input. The child component calls a callback prop (e.g., onToggle, onSubmit) passed from the parent.
Parent updates state
The callback in the parent calls setState with new data. React (or Vue/Angular) detects the state change and schedules a re-render.
Component tree re-renders
The parent re-renders with new state, passes updated props to children, and the UI reflects the change. The cycle is complete — always top-down, always predictable.
Component Data Flow: ┌─────────────────────────────────────────┐ │ App (state) │ │ │ │ │ ┌───────┴────────┐ │ │ ▼ ▼ │ │ ┌──────────┐ ┌────────────┐ │ │ │ Header │ │ Content │ │ │ │ (props) │ │ (props) │ │ │ └──────────┘ │ │ │ │ │ ┌───────┐ │ │ │ │ │ List │ │ │ │ │ │(props)│ │ │ │ │ └──┬────┘ │ │ │ │ │ │ │ │ │ ┌──▼────┐ │ │ │ │ │ Item │ │ │ │ │ │(props)│ │ │ │ │ └───────┘ │ │ │ └────────────┘ │ └─────────────────────────────────────────┘ Props flow DOWN ↓ (parent → child) Events flow UP ↑ (child → parent via callbacks) State changes trigger re-renders DOWN ↓ vs MVC where data can flow in ANY direction: Model ↔ View ↔ Controller ↔ Model (bidirectional, hard to trace)
// Parent owns the state function SearchPage() { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const handleSearch = async (searchText) => { setQuery(searchText); const data = await fetchResults(searchText); setResults(data); }; return ( <div> {/* Props DOWN: pass callback and current query */} <SearchBar query={query} onSearch={handleSearch} /> {/* Props DOWN: pass results data */} <ResultsList results={results} /> </div> ); } // Child receives props, calls callback UP function SearchBar({ query, onSearch }) { return ( <input value={query} onChange={(e) => onSearch(e.target.value)} // Event UP ↑ placeholder="Search..." /> ); } // Child receives props, renders data function ResultsList({ results }) { return ( <ul> {results.map(item => ( <li key={item.id}>{item.title}</li> ))} </ul> ); }
Predictability is the superpower
When something goes wrong in a component app, you trace the data: which component owns the state? What props did it pass? What callback was called? The unidirectional flow means there's exactly one path to follow. In MVC, a bug could originate in any layer and cascade unpredictably.
MVC vs Component Architecture
This is the comparison interviewers expect you to articulate clearly. The differences go beyond syntax — they affect how teams work, how code scales, and how applications are debugged.
| Dimension | MVC | Component Architecture |
|---|---|---|
| Organization | By technical layer (models/, views/, controllers/) | By feature/component (SearchBar/, Cart/, Header/) |
| Unit of abstraction | Layer (all models, all views, all controllers) | Component (self-contained UI unit) |
| Data flow | Bidirectional (Model ↔ View ↔ Controller) | Unidirectional (props down, events up) |
| State management | Centralized in Models | Distributed across components (or lifted to shared state) |
| Reusability | Difficult — view depends on model and controller | Easy — component is self-contained, just pass props |
| Scalability | Controllers become god objects as app grows | Components compose — complexity is distributed |
| Testing | Must mock across layers (model + view + controller) | Test component in isolation — render, assert, done |
| Team scaling | Teams organized by layer (model team, view team) | Teams organized by feature (search team, cart team) |
| Debugging | Trace across 3 layers to find the bug | Bug is usually in one component — check state and props |
| Modern frameworks | Backbone.js, Angular.js (v1), Ember.js | React, Vue, Angular (v2+), Svelte, Solid |
Feature: User Profile Card (shows name, avatar, edit button) ── MVC Approach ────────────────────────────────── models/userModel.js → fetches user data, stores state views/userProfileView.js → renders HTML template controllers/userCtrl.js → handles edit click, calls model templates/userProfile.html → HTML markup styles/userProfile.css → styling 5 files across 5 directories. To understand the feature, you must read all 5 and understand how they connect. ── Component Approach ──────────────────────────── components/UserProfile/ UserProfile.tsx → markup + logic + state UserProfile.module.css → styling useUserData.ts → data fetching hook (optional) 2-3 files in ONE directory. Everything for the feature is co-located. Import <UserProfile /> anywhere.
The interview answer
"MVC organizes by technical layer, component architecture organizes by feature. For modern interactive UIs, component architecture is preferred because it provides better encapsulation, easier reuse, unidirectional data flow, and scales better with team size. MVC is still valuable for server-rendered applications where the request-response cycle maps naturally to the Controller pattern."
Real-World Example
Let's architect a dashboard with a sidebar, search bar, data table, and stats cards — using both approaches. This makes the differences concrete.
Project Structure: models/ dashboardModel.js → all dashboard data (stats, table rows, filters) searchModel.js → search query, results userModel.js → current user info views/ sidebarView.js → renders sidebar navigation searchBarView.js → renders search input dataTableView.js → renders table with rows statsCardView.js → renders stat cards controllers/ dashboardController.js → orchestrates EVERYTHING searchController.js → handles search logic Data Flow: User types in search → searchBarView captures input → searchController.handleSearch(query) → searchModel.setQuery(query) → searchModel.fetchResults() → searchModel notifies listeners → dataTableView.render(searchModel.results) → statsCardView.render(dashboardModel.filteredStats) Problems: • dashboardController grows to 400+ lines • searchModel change affects dataTableView AND statsCardView • Adding a new widget means touching model + view + controller • Two developers can't work on search and stats independently
// Project Structure: // components/ // Dashboard/ // Dashboard.tsx → layout composition // Sidebar/ // Sidebar.tsx → self-contained navigation // SearchBar/ // SearchBar.tsx → search input + logic // useSearch.ts → search data hook // DataTable/ // DataTable.tsx → table rendering // DataTableRow.tsx → single row component // StatsCard/ // StatsCard.tsx → individual stat display function Dashboard() { const [searchQuery, setSearchQuery] = useState(""); return ( <div className="dashboard"> <Sidebar /> <main> <SearchBar query={searchQuery} onSearch={setSearchQuery} /> <div className="grid"> <StatsCard title="Users" query={searchQuery} /> <StatsCard title="Revenue" query={searchQuery} /> </div> <DataTable query={searchQuery} /> </main> </div> ); } // Each component is independent: // • Sidebar team works on Sidebar/ — no conflicts // • Search team works on SearchBar/ — isolated // • DataTable can be reused on other pages // • Adding a new widget = adding a new component // • No "god controller" — logic is distributed
| Aspect | MVC Dashboard | Component Dashboard |
|---|---|---|
| Files to add a widget | 3+ (model, view, controller) | 1-2 (component + optional hook) |
| Files to understand search | 5 (across 3 directories) | 2 (in SearchBar/ directory) |
| Team independence | Low — shared controllers | High — isolated component directories |
| Reuse on another page | Extract model + view + controller | Import <DataTable /> and pass props |
| Testing | Mock model, controller, DOM | Render component, assert output |
Why the industry shifted
The dashboard example illustrates why every major frontend framework adopted component architecture. As UIs became more interactive (real-time updates, drag-and-drop, complex forms), the MVC controller became a bottleneck. Components distribute complexity naturally — each piece of UI manages itself.
When to Use What
Neither pattern is universally better. The right choice depends on the type of application, the team, and the level of interactivity required.
Is the UI highly interactive (real-time, drag-drop, complex state)? │ ├─ YES → Component Architecture (React, Vue, Angular 2+) │ • State is distributed across components │ • Unidirectional data flow │ • Easy to compose and reuse UI pieces │ └─ NO → Is the server rendering most of the HTML? │ ├─ YES → MVC is natural (Rails, Django, Laravel) │ • Controller handles one request at a time │ • View is a template rendered on the server │ • Model maps to database tables │ └─ NO → Hybrid approach • Server-side MVC for routing and data • Component architecture for interactive UI islands • Example: Rails + React, Django + Vue
| Use Case | Recommended Pattern | Why |
|---|---|---|
| Modern SPA (React, Vue, Angular) | Component Architecture | Complex state, high interactivity, team scaling |
| Server-rendered app (Rails, Django) | MVC | Request-response cycle maps naturally to Controller |
| Simple CRUD admin panel | Either works | Low complexity — architecture matters less |
| Real-time dashboard | Component Architecture | Many independent UI pieces updating simultaneously |
| E-commerce with server rendering | Hybrid (MVC + Components) | Server MVC for pages, components for interactive parts (cart, search) |
| Mobile app (React Native, Flutter) | Component Architecture | UI is inherently component-based on mobile |
The pragmatic answer
In 2024+, component architecture is the default for frontend development. MVC is still relevant for server-side rendering and as a mental model for backend APIs. Many real-world applications use both: MVC on the server (routing, data access) and components on the client (interactive UI).
Performance & Maintainability
Architecture choices have direct impact on performance, maintainability, and developer productivity. Here's how the two patterns compare in practice.
Targeted Re-renders
Components only re-render when their props or state change. In MVC, a model change can trigger re-renders across all subscribed views. Component architecture enables granular updates via React.memo and virtual DOM diffing.
Code Splitting
Components are natural code-split boundaries. Lazy-load a component and its entire feature loads on demand. In MVC, splitting by layer is harder — you'd need to split model + view + controller together.
Tree Shaking
Unused components are automatically removed by bundlers. In MVC, unused models or controllers may still be bundled if they're imported by a shared controller. Component architecture naturally produces smaller bundles.
Developer Velocity
Co-located code (markup + logic + styles in one directory) means developers spend less time navigating between files. Feature-based organization reduces context-switching and helps new developers onboard faster.
Scenario: Fix a bug in the search feature ── MVC ────────────────────────────────────────── 1. Open controllers/searchController.js — is the bug here? 2. Open models/searchModel.js — is the data wrong? 3. Open views/searchView.js — is the rendering wrong? 4. Open templates/search.html — is the markup wrong? 5. Check how searchController interacts with dashboardController 6. Check which other views subscribe to searchModel → 6+ files across 4+ directories, complex dependency chain ── Component Architecture ─────────────────────── 1. Open components/SearchBar/SearchBar.tsx 2. Check state, props, and render output 3. If data issue → check useSearch.ts hook → 1-2 files in 1 directory, isolated from other features Scenario: Delete the search feature entirely ── MVC ────────────────────────────────────────── Delete searchModel, searchView, searchController, search template But wait — does dashboardController reference searchModel? Does another view subscribe to searchModel events? → Risky deletion, requires careful dependency analysis ── Component Architecture ─────────────────────── Delete the SearchBar/ directory Remove <SearchBar /> from the parent component → Clean deletion, no hidden dependencies
The maintenance tax
Over the lifetime of a project, maintenance is 60-80% of total cost. Architecture that makes changes safe and localized pays for itself many times over. Component architecture's co-location principle directly reduces the cost of every bug fix, feature addition, and refactor.
Common Mistakes
Even with component architecture, developers often fall into patterns that recreate MVC's problems. Watch for these anti-patterns.
Creating 'controller' components
Building components that contain only logic and no UI — essentially recreating MVC controllers inside a component tree. These 'god components' orchestrate everything and become bottlenecks.
✅Distribute logic using custom hooks (useSearch, useAuth). Keep components focused on rendering. Use composition to combine behavior, not inheritance or controller layers.
Organizing components by type instead of feature
Creating folders like components/, hooks/, utils/, styles/ — grouping by technical role instead of feature. This is MVC thinking applied to component architecture.
✅Co-locate by feature: SearchBar/SearchBar.tsx, SearchBar/useSearch.ts, SearchBar/SearchBar.css. Everything for a feature lives together.
Prop drilling through many layers
Passing props through 5+ levels of components because state lives too high in the tree. This creates tight coupling between distant components — similar to MVC's model-view coupling.
✅Use Context API for truly global state (theme, auth). Use state management (Zustand, Redux) for shared state. Keep state as close to where it's used as possible (colocation).
Mixing MVC patterns into React
Creating separate 'model' classes, 'service' layers, and 'controller' hooks that mirror MVC structure. This adds unnecessary abstraction and fights the component model.
✅Embrace the component paradigm. State lives in components or hooks. Data fetching lives in hooks or server components. There's no need for a separate 'model' layer in most React apps.
Components that are too large
Building 500+ line components that handle multiple concerns — rendering, data fetching, form validation, error handling. This is the component equivalent of a god controller.
✅Extract sub-components for distinct UI sections. Extract custom hooks for logic. A component should ideally do one thing well. If it has multiple responsibilities, split it.
Tightly coupled sibling components
Two sibling components that directly reference each other's internal state or call each other's methods. This recreates the bidirectional coupling that MVC suffers from.
✅Siblings communicate through their shared parent. Parent owns the shared state and passes it down as props. This maintains unidirectional data flow.
Interview Questions
These questions test architectural thinking — not framework syntax. Strong answers explain trade-offs and justify decisions with reasoning.
Q:What is MVC and how does it work?
A: MVC (Model-View-Controller) separates an application into three layers. The Model manages data and business logic. The View renders the UI. The Controller handles user input, updates the Model, and coordinates the View. Data flows: User → Controller → Model → View. It was designed for desktop apps (Smalltalk, 1979) and became dominant in server-side web frameworks (Rails, Django). On the frontend, frameworks like Backbone.js and Angular.js (v1) used MVC.
Q:How is React different from MVC?
A: React uses component architecture, not MVC. In MVC, code is organized by technical layer (models/, views/, controllers/). In React, code is organized by feature — each component encapsulates its own markup (JSX), logic (hooks/handlers), and state. Data flows unidirectionally (props down, events up) instead of bidirectionally. There's no separate 'controller' — components handle their own logic. React's model is closer to 'UI = f(state)' than to MVC's layer separation.
Q:Why did the industry shift from MVC to component architecture?
A: Three main reasons: (1) MVC's bidirectional data flow became unpredictable in complex UIs — Facebook's cascading update problem. (2) Controllers became god objects as features grew. (3) Reusability was poor — extracting a feature required pulling code from 3+ layers. Component architecture solved these by encapsulating features, enforcing unidirectional flow, and making composition the primary abstraction.
Q:Can you use MVC in a React application?
A: You can, but it fights the framework. Some teams create 'model' classes, 'service' layers, and 'controller' hooks — but this adds abstraction without benefit. React's hooks and component model already handle what MVC layers do: useState/useReducer for models, JSX for views, event handlers for controllers. The better approach is to embrace component architecture and use hooks for shared logic.
Q:What are the advantages of component architecture over MVC for frontend?
A: Encapsulation (each component is self-contained), reusability (import and use anywhere), unidirectional data flow (predictable, debuggable), co-location (feature code lives together), team scaling (teams own components, not layers), easier testing (render component, assert output), and natural code splitting (lazy-load by component). MVC's advantages — clear separation of data and UI — can still be achieved within components using hooks.
Q:How do you handle shared state in component architecture?
A: Three strategies based on scope: (1) Lift state up — move state to the nearest common ancestor and pass via props. Works for 2-3 levels. (2) Context API — for truly global state (theme, auth, locale). Avoids prop drilling but re-renders all consumers on change. (3) State management library (Redux, Zustand, Jotai) — for complex shared state with fine-grained subscriptions. The principle: keep state as close to where it's used as possible.
Q:Is MVC still relevant in modern development?
A: Yes, but primarily on the server side. Server-side MVC (Rails, Django, Spring) maps naturally to the request-response cycle: Controller handles the route, Model queries the database, View renders the response. Many modern apps use a hybrid: MVC on the server (API routes, data access) and component architecture on the client (interactive UI). MVC is also a useful mental model for understanding separation of concerns, even if you don't use it directly.
Q:How would you explain the difference to a junior developer?
A: MVC is like organizing a kitchen by tool type: all knives in one drawer, all pots in another, all ingredients in the pantry. To make a sandwich, you visit 3 places. Component architecture is like organizing by meal: everything for sandwiches in one box, everything for salads in another. To make a sandwich, you open one box. Both are organized — but the second approach is faster when you're cooking (building features).
Practice Section
These scenarios test your ability to apply architectural thinking to real-world problems — the kind of reasoning interviewers value most.
You're building a large e-commerce platform with product listings, search, cart, checkout, user profiles, and an admin dashboard. The team has 15 frontend developers split into feature teams.
How would you structure the frontend architecture?
Answer: Component architecture with feature-based organization. Each team owns a feature directory (Search/, Cart/, Checkout/, Admin/). Shared UI primitives (Button, Input, Modal) live in a shared components library. State management via Zustand or Redux for cross-feature state (cart items, auth). Each feature is independently deployable and testable. MVC would fail here because 15 developers touching shared controllers would cause constant merge conflicts.
A legacy Backbone.js (MVC) application needs to be modernized. It has 200+ models, views, and controllers. The team wants to migrate to React but can't rewrite everything at once.
How would you approach the migration?
Answer: Incremental migration using the strangler fig pattern. Keep the Backbone MVC shell but replace individual views with React components, one feature at a time. Start with leaf components (no dependencies on other views). Use a bridge layer to pass Backbone model data as React props. Over time, replace Backbone models with React state/hooks. Each migrated feature becomes a self-contained React component. This avoids a risky big-bang rewrite while gradually moving to component architecture.
Your React application has grown to 300+ components. Developers complain that it's hard to find things. The current structure is: components/ (all 300 components flat), hooks/ (50 hooks), utils/ (30 utilities).
What's wrong and how would you fix it?
Answer: The structure is organized by technical type (MVC thinking applied to components). Fix: reorganize by feature. Group related components, hooks, and utils into feature directories. Example: features/Search/ contains SearchBar.tsx, SearchResults.tsx, useSearch.ts, searchUtils.ts. Shared primitives stay in a ui/ directory. This co-location means developers working on search only need to look in one place. Add barrel exports (index.ts) for clean imports.
You're designing a real-time collaborative document editor (like Google Docs). Multiple users edit simultaneously, with cursor positions, text changes, and comments syncing in real-time.
Would you use MVC or component architecture? Why?
Answer: Component architecture, without question. Each UI element (toolbar, editor canvas, comment sidebar, cursor overlay, user presence indicator) is an independent component with its own state and rendering logic. Real-time updates from WebSocket are distributed to only the affected components. MVC would struggle because a single model change (new character typed) would need to update multiple views (editor, cursor, presence) through a controller — the bidirectional flow would make conflict resolution nearly impossible to debug.
A team lead argues that React components violate separation of concerns because JSX mixes HTML and JavaScript in the same file. They want to separate templates from logic, like MVC does.
How would you respond?
Answer: The concern is valid but based on a different definition of 'separation.' MVC separates by technology (HTML, JS, CSS). Component architecture separates by concern/feature (everything for SearchBar together). JSX doesn't mix concerns — it co-locates related concerns. The markup IS the component's concern. True separation of concerns means a change to the search feature doesn't affect the cart feature — which component architecture achieves better than MVC. You can still separate logic from rendering using custom hooks.
Cheat Sheet
Quick-reference summary for interviews and architecture discussions.
Quick Revision Cheat Sheet
MVC stands for: Model (data) + View (UI) + Controller (input handling)
Component architecture: UI split into self-contained, reusable units with own markup, logic, and state
MVC data flow: Bidirectional — Model ↔ View ↔ Controller (hard to trace in complex UIs)
Component data flow: Unidirectional — props down, events up (predictable, debuggable)
MVC organizes by: Technical layer (models/, views/, controllers/)
Components organize by: Feature (SearchBar/, Cart/, UserProfile/)
MVC weakness in frontend: Controllers become god objects, bidirectional flow causes cascading updates
Component strength: Encapsulation, reusability, composition, team scaling
MVC still good for: Server-side apps (Rails, Django), simple CRUD, request-response cycle
Components good for: SPAs, interactive UIs, real-time apps, large teams
Why industry shifted: Facebook's cascading update problem → Flux → Redux → React component model
Reuse in MVC: Extract model + view + controller + template (complex)
Reuse in components: Import component, pass props (simple)
Testing in MVC: Mock across layers (model + view + controller)
Testing components: Render in isolation, assert output, check state
Hybrid approach: MVC on server (routing, data) + components on client (interactive UI)