Frontend UI Architecture Patterns — MVC, MVP, MVVM, Flux
A record of the emergence, advantages, and disadvantages of UI architecture patterns (MVC, MVP, MVVM, Flux), and their comparative analysis.
A record of the emergence, advantages, and disadvantages of UI architecture patterns (MVC, MVP, MVVM, Flux), and their comparative analysis.
The password you enter is used to open, edit, and delete secret comments.
As frontend projects grow, the question of "where to manage state and how to propagate it" becomes more significant than "how to implement it." While features work, if the flow of state changes becomes unclear, debugging, scaling, and collaboration simultaneously become challenging.
MVC, MVP, MVVM, and Flux are patterns designed to solve these issues. Each pattern was created to address the shortcomings of its predecessor, inadvertently introducing new challenges, leading to the current trend.
| Pattern | Issue Solved | New Issue Created |
|---|---|---|
| MVC |
| Separation of Concerns |
| Bloated Controller |
| MVP | Simplified View, Testing Ease | Bloated Presenter |
| MVVM | Productivity, Declarative UI | Difficulty in State Tracking |
| Flux | Predictable State Flow | Increased Code Complexity |
This pattern first emerged in the 1970s in Smalltalk. It divides into three roles: Model (data/business logic), View (rendering), and Controller (handling user input).
function addComment(text) {
model.add(text); // Update Model
view.render(model.comments); // Pass data to View → View handles rendering
}The Controller updates the Model and passes data to the View, which then handles it. This structure aligns well with server-side frameworks like Rails, Django, and Spring MVC that receive requests, process them, and return HTML.
The issue is that the Controller needs to know too much. All user input, Model states, and data to pass to the View accumulate within the Controller. As the number of screens increases, the Controllers become bloated, and dependencies between View and Model get complex. This issue was exacerbated as it transitioned to the client side.
This pattern emerged to resolve issues in MVC's Controller. It consists of Model (data/logic), View (just rendering), and Presenter (handles all UI logic).
The critical difference from MVC is that the View only performs rendering. In MVC, the View handled the data itself, but in MVP, the View relays user input to the Presenter and renders only as directed by it. There is no logic involved.
// View — Only relays input and performs rendering
button.onClick(() => presenter.handleAdd(input.value));
showComments(comments) { list.innerHTML = render(comments); }
// Presenter — Handles all UI logic
handleAdd(text) {
this.model.add(text);
this.view.showComments(this.model.comments); // Directs the View
}Since the View is completely passive, the Presenter can be tested without the UI. This means you can verify if "the Model updates correctly when input is received" without the View. This pattern remains useful when clear testing is needed or when Views need to be kept as simple as possible.
In early Android development, it was widely used due to its testability. Android's Activity/Fragment structure often played both View and Controller roles, leading to bloat. By separating out the Presenter with MVP, Activities focused only on rendering, and logic could be quickly tested with JUnit. Conversely, iOS promoted MVC as the official pattern with UIKit, but the Massive View Controller issue was severe, leading to the frequent use of MVVM with RxSwift. Now, with the shift to SwiftUI, MVVM has become the de facto standard.
However, concentrating all flows in the Presenter eventually repeats the issue of the Presenter becoming bloated.
This is a pattern designed by Microsoft for WPF (Windows Presentation Foundation, a Windows desktop app UI framework). It is structured as Model (data · logic), View (display), and ViewModel (holds the state needed by the View).
Instead of the "Presenter directly instructs the View" approach, the core idea is to let the View react automatically when the state changes. The ViewModel holds the state necessary for the View, and the View binds to that state, updating automatically.
const [comments, setComments] = useState([]);
const handleAdd = (text) => {
setComments(prev => [...prev, text]); // The View reacts automatically when only the state is changed
};Rather than directly controlling "how should the View be updated," this approach declares "the state should change like this." React, SwiftUI, and Jetpack Compose are all based on this idea.
The advantage is high productivity and cleaner code, but since you can change the state from anywhere, it becomes difficult to track "why is this state like this?" When there are many components and the state starts flowing through multiple layers, this problem becomes immediately apparent.
This architecture was announced by Facebook in 2014 along with React. The background for its development was quite specific: there was a bug in the Facebook app where the notification number was displayed incorrectly. The structure allowed the state to be changed in multiple places, making it very hard to find the cause, so the idea to fix the state change path to only one was introduced.
Action → Dispatcher → Store → View → (back to Action)To change the state, you must dispatch an Action. The Store receives this Action and updates the state, and the View subscribes to the updated state. There is no path for the View to directly tamper with the state.
dispatch({ type: 'ADD_COMMENT', payload: text });Since all state changes go through the single door of Action, it's easy to trace what happened in what order. This structure allows Redux DevTools to perform time-travel debugging. The downside is the enforced procedure. Even for a simple state change, you need to define an Action, write a Reducer, and connect to the Store, which makes it over-engineered for small projects.
React is neither MVVM nor Flux. Calling setState makes the UI update automatically, which feels like data binding, but what React actually does is different.
Call setState()
→ Schedule rendering
→ Process batched updates
→ Virtual DOM diff
→ Reflect minimal changes only to the actual DOMReact does not enforce a specific state management approach. You can use it like MVVM with useState, or like Flux with Redux or Zustand. Because it separates the rendering engine from state management, both are possible.
Vue was different. Up to Vue 2, Vuex was pushed as the official state management library, and it strictly follows the Flux architecture. A unidirectional flow consisting of State, Mutation, Action, and Getter is enforced. With Vue 3, Pinia became the official alternative. Pinia removed Mutation, integrating it into Action, becoming much lighter while maintaining the core of Flux's unidirectional flow.
Zustand is a library used in React that supports both patterns. You can directly set the state from anywhere, like in MVVM, or explicitly define an Action pattern, like in Flux. It is much lighter and with less boilerplate than Redux, making it a popular choice in cases where "Flux is needed but Redux is too heavy."
These days, rather than unifying state into one pattern, it's common to manage it according to its nature.
| State Type | Example | Management Method |
|---|---|---|
| Server State | API response data | TanStack Query |
| UI State | Modal open/close, toggle | useState |
| Global State | Authentication info, theme | Zustand / Redux |
Putting server state into a global store complicates caching strategies, and raising UI state globally causes unnecessary re-renders. The idea of "managing all states in one place" is an approach that no longer works well.
If quickly responding to UI state and managing state per component are key, the MVVM approach feels more natural. For small-scale, rapid prototyping and when a declarative UI is crucial, starting with a useState-based approach is correct. On the other hand, if you need to clearly control the flow of state changes or there are complex global states shared by multiple components, the Flux pattern is appropriate. In summary, use MVVM for simple states, and Flux for complex global states.
In practice, many start with useState and then partially introduce Zustand when locating state becomes difficult or reproducing bugs becomes challenging. There's no need to choose the "right answer" from the start; switch from the parts where problems arise.
The question "Is the current state flow controllable?" signals when it's time for a pattern shift.
A record of the intense 7-week journey: from improving onboarding and infrastructure monitoring to implementing real-time sockets and 3D character optimization.
A record of exploring the pros and cons of functional programming and object-oriented programming, and the reasons for choosing functional programming in React
A record of the emergence, advantages, and disadvantages of UI architecture patterns (MVC, MVP, MVVM, Flux), and their comparative analysis.
A record of the issues faced when putting characters on Three.js for the Funda project and personal portfolio
A 7-month journey from Basic to Group Projects: mastering CS fundamentals, discovering the essence of software design, and evolving through AI engineering
A record of the intense 7-week journey: from improving onboarding and infrastructure monitoring to implementing real-time sockets and 3D character optimization.
A record of building the foundation: from ideation and prototyping to navigating senior feedback and implementing the MVP
An in-depth look at why we normalize vectors, connecting game movement logic with Blender's "Apply Scale."
A reflection on a 6-week team sprint: covering architectural challenges, the nuances of collaboration, and technical growth through senior feedback.
A reflection on the 10-week Boostcamp membership sprint: technical learning, design challenges, burnout, and lessons about using AI effectively.
A personal retrospective on the Boostcamp Web·Mobile Challenge phase: daily missions, CS learning, peer feedback, teamwork, and how I learned to grow alongside AI
A record of exploring the pros and cons of functional programming and object-oriented programming, and the reasons for choosing functional programming in React