Why I Studied This
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.
What Confused Me
The Acronym Chaos
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?
The Mystery of ViewModel
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...?
What Even Is Data Binding
"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?
MVP vs MVVM Differences
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?
The Aha Moment: "Remote Control"
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!"
Another Metaphor: Restaurant
I also thought about restaurants.
MVC (Traditional Restaurant)
- Customer (View): "I'll have the steak"
- Waiter (Controller): Takes order to kitchen (Model)
- Kitchen: Cooks food
- Waiter: Brings food to customer
- Problem: Waiter handles every table's orders, delivery, cleanup → Massive View Controller
MVP (Improved Restaurant)
- Customer (View): Only talks to waiter (Presenter)
- Waiter: Communicates with kitchen, updates customer
- Customer doesn't know the kitchen. Only knows the waiter.
- Still manual: Waiter must actively say "food's ready", "want water refill?"
MVVM (Smart Restaurant)
- Customer (View): Orders via tablet (ViewModel) at table
- Tablet screen: Shows real-time order status ("Cooking - 3 min left")
- Kitchen (Model) status changes → tablet auto-updates
- Data Binding: Kitchen-tablet connected. No waiter needed for status updates
This metaphor clarified what "auto-sync" really means.
MVC's Problem: Fat Controllers
Massive View Controller Hell
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.
- View logic (UI updates)
- Business logic (validation, data processing)
- Network logic
- Routing logic
- Delegate pattern implementations
- Error handling
Why This Happens
Looking at MVC's structure:
View → Controller ← Model
Controller knows both View and Model. So naturally all logic gravitates to Controller.
- Button pressed in View? → Controller handles it
- Model data changed? → Controller updates View
- Need validation? → Controller does it
- Network request? → Controller handles it
- Screen navigation? → Controller manages it
Eventually Controller becomes the project's trash can.
Real Problems I Faced
1. Untestable
// 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:
- Actual UITextField instances
- Real network requests
- A screen to show alerts
Unit testing impossible. UI, business logic, and network are all tangled together.
2. Unreusable
Want 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 Hell
Teammate 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.
MVP (Model-View-Presenter): The Mediator Arrives
Core Idea
Completely separate View and Model. All communication only through Presenter.
View ←→ Presenter ←→ Model
Critical rules:
- View knows nothing about Model
- Model knows nothing about View
- Only Presenter knows both
Actual Implementation
// 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;
Why This Is Better
1. Testable Presenter
// 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 Views
Reuse 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 Concerns
View : "Button pressed", "Display this text" (UI)
Presenter : "Validate email", "Save logic" (Business logic)
Model : "Save data", "API calls" (Data)
Clear responsibilities for each.
But Still Problems
Manual Update Tedium
// 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 (Model-View-ViewModel): Auto-Sync Magic
Core: Data Binding
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.
What's ViewModel
"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.
Data Binding with Vue.js
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- Type in input → name variable auto-updates
- name variable changes → input value auto-updates
-
{{ name }}: Display name variable on screen- name changes →
<p>content auto-updates
- name changes →
-
When
this.name = 'Alice'executes:<input>value becomes 'Alice'<p>content becomes 'Hello, Alice!'- Automatically!
How It Works (Reactivity System)
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.
MVVM in React
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:
setUsercalled- React: "Oh, state changed? Re-render this component"
- Re-execute function component from scratch
- Generate new Virtual DOM
- Compare with previous Virtual DOM (Diffing)
- Apply only changed parts to actual DOM
Unlike Vue, doesn't track "exactly which value changed". Instead re-renders entire component (fast thanks to Virtual DOM).
Why useState Is MVVM
const [count, setCount] = useState(0); // count = ViewModel
count: ViewModel (data to display)setCount: Update ViewModel → View auto-updates- JSX
{count}: Bind ViewModel to View
No more manual DOM manipulation.
// 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.
SwiftUI: Declarative MVVM Extreme
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.
Jetpack Compose (Android): Kotlin's MVVM
@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).
Pros and Cons Comparison
MVP Pros
1. Perfect Separation
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();
MVP Cons
1. Boilerplate Hell
// 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 Cost
Every value change requires manually calling View methods.
MVVM Pros
1. Auto UI Update
// 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.
MVVM Cons
1. Hard to Debug
// 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).
3. Learning Curve
// 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();
}, []);
MVC vs MVP vs MVVM Final Comparison
| 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 |
Real-World Experience
Android Migration: MVP → MVVM
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
- Add method to View interface
- Call method in Presenter
- Implement method in Activity
After (MVVM + LiveData)
// 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:
- Deleted View interface (removed 100 lines)
- Deleted Presenter's manual method calls (removed 50 lines)
- LiveData handles auto-update
Code reduced 40%.
React Migration: jQuery → React
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:
- 800 lines → 120 lines (85% reduction)
- DOM manipulation code completely gone
- Change one state → all related parts auto-update
Initially thought "will this really work?" It did. Way more stable too.
Why Bugs Disappeared
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.
When to Use What
Use MVC When
- Backend web frameworks (Rails, Django, Laravel)
- Server-side rendering
- Frontend is simple (no complex state management needed)
Use MVP When
- Legacy Android projects (already written in MVP)
- Java/Kotlin before Jetpack Compose adoption
- Very strict View-Model separation required
- Test coverage is top priority
Honestly rarely used in new projects. MVVM is better.
Use MVVM When
- Modern frontend (React, Vue, Svelte)
- Modern mobile (SwiftUI, Jetpack Compose, Flutter)
- Complex UI state management
- Real-time updates (chat, dashboards)
- Development speed matters
2025 frontend standard.
Key Takeaways
What I clearly understood from this study:
1. MVP is "Perfect Separation"
View and Model completely unaware of each other. Presenter mediates. Great for testing but lots of code.
2. MVVM is "Auto-Sync"
Data Binding means change ViewModel → View auto-updates. Manual DOM manipulation gone.
3. React's useState is MVVM
const [state, setState] = useState(initialValue);
state: ViewModelsetState: Update ViewModel → auto re-render- JSX: Bind View and ViewModel
4. Modern Frontend = MVVM
React, Vue, Angular, Svelte, SwiftUI, Compose... all MVVM-based.
5. "Change Variable → Screen Changes"
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.
Final Advice
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.