
MVP & MVVM: Making Views Smarter
MVC's Controller got too fat. Split into Presenter/ViewModel, auto-update with Data Binding. Core pattern of modern frontend.

MVC's Controller got too fat. Split into Presenter/ViewModel, auto-update with Data Binding. Core pattern of modern frontend.
Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Tired of naming classes? Writing CSS directly inside HTML sounds ugly, but it became the world standard. Why?

Establishing TCP connection is expensive. Reuse it for multiple requests.

Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

When I first learned React, useState blew my mind.
const [count, setCount] = useState(0);
setCount(count + 1); // DOM auto-updates
I'd been coding with jQuery for 3+ years. I learned that updating the UI meant this:
$('#counter').text(count);
$('#status').html('Updated!');
$('#timestamp').text(new Date());
For every button click, I manually updated 10+ DOM elements. But React said "just change the variable, I'll handle the rest." It felt like magic.
A senior dev in my community said "that's MVVM. All modern frontend works this way."
So I dug in. MVC, MVP, MVVM... they all have Model-View, but what's actually different? And why did jQuery become "old-school"? Why did everyone switch to React/Vue? I wanted to understand exactly why.
MVC, MVP, MVVM... they all start with Model-View and differ by one letter. Initially I thought "are these just different names for the same pattern?" They all have Model and View, right?
ViewModel confused me the most. "Model for the View" - but how's that different from a regular Model? They're both just data, aren't they?
// Model
const user = { id: 1, name: 'John' };
// ViewModel
const userViewModel = { name: 'John' };
// Wait, what's the difference...?
"Auto-sync with Data Binding" sounded abstract. What does "sync" mean here? How's it different from just assigning a variable?
let name = 'Alice';
// Is this Data Binding? Or not?
Presenter vs ViewModel... both sit between View and Model. Diagrams showed:
MVP: View ←→ Presenter ←→ Model
MVVM: View ←→ ViewModel ←→ Model
They look identical! Just different names?
A senior engineer explained it with a metaphor that clicked instantly.
MVC: TV (Model) and remote control (Controller) are completely separate.
- Press button → Controller sends signal to Model → TV screen changes
- You can't tell the current channel by looking at the TV. Remote shows nothing either.
MVP: The remote (Presenter) got smarter.
- Press button → Presenter tells View "display 9"
- Then Presenter tells Model (TV) "switch to channel 9"
- Still manual. Presenter has to command everything step-by-step.
MVVM: Remote has a tiny screen (ViewModel) built-in.
- Press 9 on remote screen → TV automatically switches to channel 9
- This is Data Binding. Remote screen and TV are "connected."
- Change one side → other side auto-updates!
That metaphor made me go "OH, so that's why React's setCount auto-updates the screen!"
I also thought about restaurants.
MVC (Traditional Restaurant)This metaphor clarified what "auto-sync" really means.
There's a famous joke among iOS developers.
"Know what MVC stands for?" "Model-View-Controller?" "No, Massive View Controller."
It was real. When I first took over an iOS project, the ViewController.swift file was 2,800 lines.
// ViewController.swift (2,800 lines of nightmare)
class ViewController: UIViewController {
// ===== View stuff =====
@IBOutlet var nameLabel: UILabel!
@IBOutlet var emailTextField: UITextField!
@IBOutlet var profileImageView: UIImageView!
@IBOutlet var saveButton: UIButton!
@IBOutlet var cancelButton: UIButton!
@IBOutlet var loadingSpinner: UIActivityIndicatorView!
// ===== Business logic =====
func validateEmail(_ email: String) -> Bool {
let regex = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
// ... 50 lines of validation
}
func validatePassword(_ password: String) -> Bool {
// ... 30 lines
}
func hashPassword(_ password: String) -> String {
// ... 20 lines
}
// ===== Network calls =====
func fetchUser(userId: Int) {
let url = URL(string: "https://api.example.com/users/\(userId)")!
URLSession.shared.dataTask(with: url) { data, response, error in
// ... 100 lines of network handling
}.resume()
}
func uploadProfileImage(_ image: UIImage) {
// ... 150 lines
}
// ===== UI updates =====
func updateUI() {
nameLabel.text = user.name
emailTextField.text = user.email
profileImageView.image = user.profileImage
saveButton.isEnabled = isFormValid
// ... manually updating 50+ UI elements
}
// ===== Delegate methods =====
func textFieldDidChange(_ textField: UITextField) { ... }
func textFieldDidBeginEditing(_ textField: UITextField) { ... }
func textFieldShouldReturn(_ textField: UITextField) -> Bool { ... }
// ... 30 delegate methods
}
Everything in one file.
Looking at MVC's structure:
View → Controller ← Model
Controller knows both View and Model. So naturally all logic gravitates to Controller.
Eventually Controller becomes the project's trash can.
// How do you test this function?
func saveUser() {
let name = nameTextField.text! // depends on UI
let email = emailTextField.text!
if validateEmail(email) { // business logic
let user = User(name: name, email: email)
api.save(user) { result in // network
self.showAlert("Saved!") // back to UI
}
}
}
To test this you need:
Unit testing impossible. UI, business logic, and network are all tangled together.
2. UnreusableWant to show User info on different screens?
// UserProfileViewController.swift
func displayUser() {
nameLabel.text = user.name
}
// UserDetailViewController.swift
func displayUser() {
nameLabel.text = user.name // copy-pasted same code
}
No way to reuse logic. Controller inherits from UIViewController, tightly coupled to a specific screen.
3. Collaboration HellTeammate A: editing ViewController.swift (lines 1-1000)
Teammate B: editing ViewController.swift (lines 1500-2000)
Me: editing ViewController.swift (lines 800-1200)
→ Git Merge Conflict
→ 3 hours resolving conflicts...
Everything in one file meant daily Git conflicts.
These problems led to MVP.
Completely separate View and Model. All communication only through Presenter.
View ←→ Presenter ←→ Model
Critical rules:
// Model
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
save() {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name: this.name, email: this.email })
});
}
}
// View Interface (methods View must implement)
// Presenter only communicates through this interface
interface UserView {
getNameInput(): string;
getEmailInput(): string;
displayUser(name: string, email: string): void;
showLoading(): void;
hideLoading(): void;
showError(message: string): void;
}
// Presenter
class UserPresenter {
constructor(view, model) {
this.view = view; // only knows View interface
this.model = model;
}
onSaveButtonClick() {
// Get data from View
const name = this.view.getNameInput();
const email = this.view.getEmailInput();
// Validation (business logic)
if (!this.validateEmail(email)) {
this.view.showError('Invalid email');
return;
}
// Update Model
this.model.name = name;
this.model.email = email;
this.view.showLoading();
// Save
this.model.save()
.then(() => {
this.view.hideLoading();
this.view.displayUser(name, email);
})
.catch(err => {
this.view.hideLoading();
this.view.showError(err.message);
});
}
validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// View Implementation (HTML/DOM)
class UserViewImpl {
constructor(presenter) {
this.presenter = presenter;
this.nameInput = document.getElementById('name');
this.emailInput = document.getElementById('email');
this.saveBtn = document.getElementById('save');
this.loadingSpinner = document.getElementById('loading');
this.errorDiv = document.getElementById('error');
// Wire up events
this.saveBtn.onclick = () => {
this.presenter.onSaveButtonClick();
};
}
getNameInput() {
return this.nameInput.value;
}
getEmailInput() {
return this.emailInput.value;
}
displayUser(name, email) {
this.nameInput.value = name;
this.emailInput.value = email;
}
showLoading() {
this.loadingSpinner.style.display = 'block';
}
hideLoading() {
this.loadingSpinner.style.display = 'none';
}
showError(message) {
this.errorDiv.textContent = message;
this.errorDiv.style.display = 'block';
}
}
// Usage
const model = new User('John', 'john@example.com');
const presenter = new UserPresenter(null, model);
const view = new UserViewImpl(presenter);
presenter.view = view;
// Mock View
class MockUserView {
constructor() {
this.displayedName = null;
this.displayedEmail = null;
this.errorMessage = null;
}
getNameInput() { return 'Alice'; }
getEmailInput() { return 'alice@example.com'; }
displayUser(name, email) {
this.displayedName = name;
this.displayedEmail = email;
}
showError(msg) { this.errorMessage = msg; }
showLoading() {}
hideLoading() {}
}
// Test
const mockView = new MockUserView();
const model = new User();
const presenter = new UserPresenter(mockView, model);
presenter.onSaveButtonClick();
// Verify
assert(mockView.displayedName === 'Alice');
assert(mockView.displayedEmail === 'alice@example.com');
Test Presenter logic without real UI. Just create a Mock View.
2. Swappable ViewsReuse same Presenter for web and mobile:
// Web View
class WebUserView { ... }
// Mobile View
class MobileUserView { ... }
// Same Presenter
const presenter = new UserPresenter(new WebUserView(), model);
// or
const presenter = new UserPresenter(new MobileUserView(), model);
Presenter code unchanged. Just needs Views to match the interface.
3. Separation of ConcernsView : "Button pressed", "Display this text" (UI)
Presenter : "Validate email", "Save logic" (Business logic)
Model : "Save data", "API calls" (Data)
Clear responsibilities for each.
// In Presenter
this.view.displayUser(name, email);
this.view.updateTitle(`Hello, ${name}`);
this.view.updateBadge(name[0]);
this.view.updateStatus('Online');
Every time data changes, manually call View methods. If User info displays in 10 places? Call 10 methods.
"Can't this be automatic?" I wondered. That's MVVM.
MVVM's game-changer is Data Binding.
View ⇄ ViewModel ⇄ Model
(auto-sync)
ViewModel value changes → View auto-updates View input changes → ViewModel auto-updates
Two-way auto-sync.
"Model for the View". Real meaning:
// Model (raw data from server)
const user = {
id: 1,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
created_at: '2023-01-15T08:30:00Z',
role: 'admin',
is_active: true
};
// ViewModel (transformed for View)
const userViewModel = {
fullName: `${user.first_name} ${user.last_name}`, // 'John Doe'
email: user.email,
joinDate: new Date(user.created_at).toLocaleDateString(), // '1/15/2023'
isAdmin: user.role === 'admin', // true
statusText: user.is_active ? 'Active' : 'Inactive',
badgeColor: user.is_active ? 'green' : 'gray'
};
Model is server/DB format, ViewModel is display format.
Vue.js is most intuitive, so let me show that first.
<template>
<div>
<input v-model="name" />
<p>Hello, {{ name }}!</p>
<button @click="changeName">Change</button>
</div>
</template>
<script>
export default {
data() {
return {
name: '' // ViewModel
};
},
methods: {
changeName() {
this.name = 'Alice'; // just change this
}
}
};
</script>
What happens:
v-model="name": Two-way bind input and name variable
{{ name }}: Display name variable on screen
<p> content auto-updatesWhen this.name = 'Alice' executes:
<input> value becomes 'Alice'<p> content becomes 'Hello, Alice!'I was curious "how does it auto-update?" Simplified Vue 2 implementation:
function makeReactive(obj, key, value) {
const listeners = []; // functions subscribed to this value
Object.defineProperty(obj, key, {
get() {
// Track who's reading this value
if (currentListener) {
listeners.push(currentListener);
}
return value;
},
set(newValue) {
value = newValue;
// Value changed → notify all subscribers
listeners.forEach(listener => listener());
}
});
}
// Usage example
const viewModel = {};
makeReactive(viewModel, 'name', '');
// Connect to View
let currentListener = () => {
document.getElementById('display').textContent = viewModel.name;
};
currentListener(); // initial render
// Now change value
viewModel.name = 'Alice'; // calls setter → runs listeners → DOM auto-updates
Observer Pattern. When value changes, automatically notify subscribers.
React uses a different approach. Not Data Binding but Re-rendering.
function UserProfile() {
const [user, setUser] = useState({
name: '',
email: ''
});
return (
<div>
<input
value={user.name}
onChange={e => setUser({ ...user, name: e.target.value })}
/>
<p>Hello, {user.name}!</p>
<button onClick={() => setUser({ ...user, name: 'Alice' })}>
Change
</button>
</div>
);
}
React's approach:
setUser calledUnlike Vue, doesn't track "exactly which value changed". Instead re-renders entire component (fast thanks to Virtual DOM).
const [count, setCount] = useState(0); // count = ViewModel
count: ViewModel (data to display)setCount: Update ViewModel → View auto-updates{count}: Bind ViewModel to View// Before (jQuery)
$('#count').text(count);
$('#double').text(count * 2);
$('#status').text(count > 0 ? 'Positive' : 'Zero');
// After (React)
return (
<>
<div>{count}</div>
<div>{count * 2}</div>
<div>{count > 0 ? 'Positive' : 'Zero'}</div>
</>
);
Just change ViewModel (count), React handles the rest.
iOS's SwiftUI supports MVVM at language level.
class UserViewModel: ObservableObject {
@Published var name: String = "" // @Published = "update View when this changes"
@Published var email: String = ""
}
struct UserView: View {
@ObservedObject var viewModel: UserViewModel
var body: some View {
VStack {
TextField("Name", text: $viewModel.name) // $ = two-way binding
Text("Hello, \(viewModel.name)!")
}
}
}
@Published and @ObservedObject handle Data Binding automatically. When viewModel.name changes, View auto-redraws.
@Composable
fun UserScreen(viewModel: UserViewModel) {
val name by viewModel.name.observeAsState("")
Column {
TextField(
value = name,
onValueChange = { viewModel.updateName(it) }
)
Text("Hello, $name!")
}
}
class UserViewModel : ViewModel() {
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
fun updateName(newName: String) {
_name.value = newName // change just this → View auto-updates
}
}
LiveData is Observable. Subscribe with observeAsState, value changes trigger auto-recomposition (re-rendering).
View and Model completely unaware of each other. Only Presenter knows both.
2. Testable// Test Presenter (without UI)
const mockView = new MockView();
const presenter = new Presenter(mockView);
presenter.onButtonClick();
assert(mockView.displayedData === 'expected');
3. Clear Control Flow
"Which methods get called" is explicit.
presenter.onSaveClick();
→ this.view.showLoading();
→ this.model.save();
→ this.view.hideLoading();
→ this.view.displaySuccess();
// View interface
interface UserView {
showName(name: string): void;
showEmail(email: string): void;
showAge(age: number): void;
showAddress(address: string): void;
// ... method for every field (20 fields = 20 methods)
}
// Presenter
this.view.showName(user.name);
this.view.showEmail(user.email);
this.view.showAge(user.age);
this.view.showAddress(user.address);
// ... call 20 times
2. Presenter Gets Fat Too
Eventually Presenter accumulates all logic like MVC's Controller.
3. Manual Sync CostEvery value change requires manually calling View methods.
// MVP
presenter.updateUser(newUser);
view.showName(newUser.name);
view.showEmail(newUser.email);
view.showStatus(newUser.status);
// MVVM
viewModel.user = newUser; // Done!
2. Concise Code
// MVP (100 lines)
class UserPresenter {
updateName(name) {
this.model.name = name;
this.view.showName(name);
this.view.showGreeting(`Hello, ${name}`);
this.view.showInitial(name[0]);
this.view.showNameLength(name.length);
}
}
// MVVM (10 lines)
class UserViewModel {
name = ''; // change just this → everywhere auto-updates
}
3. Declarative UI
// Imperative (MVP)
if (user.isAdmin) {
view.showAdminPanel();
view.hideUserPanel();
} else {
view.hideAdminPanel();
view.showUserPanel();
}
// Declarative (MVVM)
<div>{user.isAdmin ? <AdminPanel /> : <UserPanel />}</div>
Declare "what" to display, not "how" to display it.
// Who changed this value?
console.log(viewModel.count); // Expected: 0, Actual: 42
Auto-binding makes tracking hard. Suffer without Vue Devtools or React Devtools.
2. Performance Overhead// Cost of observing every value
const viewModel = reactive({
field1: '',
field2: '',
// ... 100 fields
});
Creating and maintaining Observables costs memory and CPU. Especially Vue 2's Object.defineProperty had performance issues (improved with Proxy in Vue 3).
// Beginners: "Why does it auto-change? Scary..."
const [count, setCount] = useState(0);
setCount(count + 1); // feels like magic
"Auto-updates" is convenient but initially hard to understand.
4. Memory Leak Risk// If you don't clean up observers properly
useEffect(() => {
const subscription = observable.subscribe(data => {
setData(data);
});
// ⚠️ memory leak without cleanup
return () => subscription.unsubscribe();
}, []);
| Item | MVC | MVP | MVVM |
|---|---|---|---|
| View-Model Relation | View can directly reference Model | Presenter mediates (complete separation) | Data Binding for auto-sync |
| UI Update | Controller manually updates | Presenter manually updates | ViewModel changes → auto-update |
| Testing | Hard (View-Model coupled) | Easy (Presenter independent) | Easy (ViewModel independent) |
| Boilerplate | Low | Very high | Medium |
| Learning Curve | Low | Medium | High (need Reactivity understanding) |
| Debugging | Easy (explicit flow) | Easy (explicit flow) | Hard (auto-binding) |
| Representative Frameworks | Ruby on Rails, Django | Android (old), .NET (WPF) | React, Vue, Angular, SwiftUI, Compose |
| Current Trend | Mainly backend | Found in legacy code | Frontend standard |
The Android project I took over 2 years ago was MVP.
Before (MVP)// UserPresenter.kt
class UserPresenter(private val view: UserView) {
fun loadUser(userId: Int) {
view.showLoading()
repository.getUser(userId) { user ->
view.hideLoading()
view.showName(user.name)
view.showEmail(user.email)
view.showProfileImage(user.imageUrl)
view.showJoinDate(formatDate(user.createdAt))
view.showBadge(if (user.isPremium) "Premium" else "Free")
// ... 20 fields
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity(), UserView {
override fun showName(name: String) {
nameTextView.text = name
}
override fun showEmail(email: String) {
emailTextView.text = email
}
// ... 20 method implementations
}
Problem: Adding one field required
// UserViewModel.kt
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
fun loadUser(userId: Int) {
_loading.value = true
repository.getUser(userId) { user ->
_loading.value = false
_user.value = user // Just this!
}
}
}
// UserActivity.kt
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.user.observe(this) { user ->
nameTextView.text = user.name
emailTextView.text = user.email
profileImageView.load(user.imageUrl)
// use user.xxx wherever needed
}
viewModel.loading.observe(this) { isLoading ->
progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
}
}
Changes:
Code reduced 40%.
Even more dramatic was jQuery → React.
Before (jQuery)// 800-line user.js
let currentUser = null;
$('#loadBtn').click(() => {
$('#loading').show();
$.get('/api/user/123', (user) => {
currentUser = user;
// Manual DOM updates (50 lines)
$('#name').text(user.name);
$('#email').text(user.email);
$('#greeting').text(`Hello, ${user.name}!`);
$('#initials').text(getInitials(user.name));
$('#memberSince').text(formatDate(user.createdAt));
$('#profileImage').attr('src', user.imageUrl);
$('#status').text(user.isOnline ? 'Online' : 'Offline');
$('#statusDot').css('background-color', user.isOnline ? 'green' : 'gray');
$('#postCount').text(user.postCount);
$('#followerCount').text(user.followerCount);
// ... continues
$('#loading').hide();
});
});
$('#editBtn').click(() => {
$('#editModal').show();
$('#editName').val(currentUser.name);
$('#editEmail').val(currentUser.email);
});
$('#saveBtn').click(() => {
const newName = $('#editName').val();
const newEmail = $('#editEmail').val();
$.post('/api/user/123', { name: newName, email: newEmail }, () => {
currentUser.name = newName;
currentUser.email = newEmail;
// Manual updates again (20 lines)
$('#name').text(newName);
$('#email').text(newEmail);
$('#greeting').text(`Hello, ${newName}!`);
$('#initials').text(getInitials(newName));
// ...
$('#editModal').hide();
});
});
After (React)
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [editMode, setEditMode] = useState(false);
const loadUser = async () => {
setLoading(true);
const data = await fetch('/api/user/123').then(r => r.json());
setUser(data);
setLoading(false);
};
const saveUser = async (updatedUser) => {
await fetch('/api/user/123', {
method: 'POST',
body: JSON.stringify(updatedUser)
});
setUser(updatedUser); // Just this!
setEditMode(false);
};
if (loading) return <Spinner />;
if (!user) return <button onClick={loadUser}>Load</button>;
return (
<div>
<img src={user.imageUrl} />
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>Hello, {user.name}!</p>
<p>{getInitials(user.name)}</p>
<p>Member since {formatDate(user.createdAt)}</p>
<StatusBadge isOnline={user.isOnline} />
<Stats posts={user.postCount} followers={user.followerCount} />
<button onClick={() => setEditMode(true)}>Edit</button>
{editMode && (
<EditModal user={user} onSave={saveUser} onCancel={() => setEditMode(false)} />
)}
</div>
);
}
Changes:
Initially thought "will this really work?" It did. Way more stable too.
In jQuery days, bugs like this were common:
// Bug: forgot to update one place
$('#saveBtn').click(() => {
currentUser.name = newName;
$('#name').text(newName); // ✅ updated
$('#greeting').text(`Hello, ${newName}!`); // ✅ updated
// ❌ forgot $('#headerName') → BUG!
});
Same data displayed in 10 places, update only 9 → bug.
React makes this bug structurally impossible:
// Change just one state
setUser({ ...user, name: newName });
// Everywhere with {user.name} auto-updates
// Can't forget
This is MVVM's real value I realized. Structure that prevents mistakes.
Honestly rarely used in new projects. MVVM is better.
2025 frontend standard.
What I clearly understood from this study:
View and Model completely unaware of each other. Presenter mediates. Great for testing but lots of code.
Data Binding means change ViewModel → View auto-updates. Manual DOM manipulation gone.
const [state, setState] = useState(initialValue);
state: ViewModelsetState: Update ViewModel → auto re-renderReact, Vue, Angular, Svelte, SwiftUI, Compose... all MVVM-based.
This is MVVM's essence. Initially felt like magic, now feels natural.
If someone asked me to go back to jQuery, I'd refuse. Don't want to do $('#element').text(value) manually everywhere.
As a non-CS founder, understanding MVP/MVVM made framework docs click.
"Why is React designed this way?" → "Oh, MVVM" "Why does Vue have v-model?" → "Oh, Data Binding" "Why does SwiftUI use @Published?" → "Oh, Observer pattern"
I accepted that design patterns are about "understanding" not "memorization". Understanding them helps learn new frameworks faster.