MVC, and what React actually is
“Is React MVC?” is a question from interviewers who learned architecture on Rails or Spring, and the standard answer — “React is just the V” — is both the right opening line and incomplete. Being able to say why is the whole value of this note.
MVC in one diagram
user ──▶ Controller ──▶ Model ──▶ View ──▶ user
│ decides │ owns │ renders
│ what │ data │ what the
└─ changes └─ rules └─ model saysModel owns data and the rules about it. View renders the model. Controller takes input and decides what to change. The value of the split is that the model has no idea a screen exists, so it can be tested, reused and changed without touching rendering. Server-side frameworks made this concrete: a route hits a controller, which loads a model, which a template renders.
Why “React is the V” is the right first sentence
React’s own description was a library for building user interfaces. A component takes props and state and returns what to render; nothing about it says where data comes from or who decides to change it. That is a view layer, and the framing tells the interviewer you know React is not a framework in the Rails sense.
// This is a view. It renders what it is given and reports intent.
function Order({ order, onCancel }: Props) {
return (
<article>
<h2>{order.id}</h2>
<button onClick={() => onCancel(order.id)}>Cancel</button>
</article>
);
}Why it is also incomplete
Two things break the clean mapping.
Components hold state. A form with useState is a view that also owns a
piece of model. React blurs V and M on purpose, because for local, ephemeral
state — is the menu open, what is in the search box — routing it through a
separate model layer is ceremony.
The senior distinction is client state versus server state: local UI state belongs in the component, and data that the server owns belongs in a cache layer that is not a component at all. See Server State vs Client State.
Data flows one way. MVC’s controller sits between input and model and often pushes back into the view. React’s model is unidirectional: state flows down as props, events flow up as callbacks, and the only way a view changes is for state above it to change. This is closer to Flux than to MVC, and Redux, Zustand and friends are all descendants of that idea.
So the honest map of a React app onto MVC vocabulary:
| MVC role | In a React app |
|---|---|
| Model | server cache (TanStack Query) plus a store |
| View | components |
| Controller | event handlers, actions, mutations |
The controller role is the one that has no single home, and that is not a gap — it is what unidirectional flow replaced.
What people mean when they build “MVC in React”
Occasionally someone hand-rolls it: a Model class with methods, a
Controller class with handlers, and components as views. It works, and it is
almost always the wrong idea, because it re-creates the coupling React was
designed to avoid — a controller object that reaches into components, a model
that has to notify views manually. The idiomatic equivalents already exist:
- A custom hook packages model-ish logic for a component:
useOrder(id)returns the data and the mutations. - A server-state library is the model for anything the server owns, with caching, invalidation and optimistic updates built in.
- A store is the model for shared client state that several components read, and its actions are the closest thing to a controller.
// The idiomatic "model": server state through a cache, not a class.
function useOrder(id: string) {
const query = useQuery({ queryKey: ["order", id], queryFn: () => fetchOrder(id) });
const cancel = useMutation({ mutationFn: cancelOrder });
return { order: query.data, cancel: () => cancel.mutate(id) };
}MVVM is the other term that comes up, usually from Angular or .NET
backgrounds: a ViewModel exposes state and commands the view binds to. A
custom hook returning { data, actions } is functionally a ViewModel, which
is a useful thing to say to someone who thinks in that vocabulary.
Where it breaks in production
- A God controller. One object that handles every event for a page, recreating the coupling the architecture was meant to remove.
- Server data in
useState. Treating server state as local state means re-implementing caching, invalidation and dedup badly. That is the model layer missing, not a React limitation. - Business rules in components. A view that computes tax is a view that cannot be tested without rendering. Pull it into a plain function.
- Two sources of truth. A hand-rolled model class and component state for the same data. Pick the owner.
Related
Interview angle 4
- “Is React MVC?” - it is the V, and saying so shows you know it is a library, not a framework. Then the correction: components also hold local state, and data flows one way, so a React app maps onto Flux more than MVC. The model is a server cache plus a store; the controller is event handlers and mutations, with no single home by design.
- “Where does business logic go?” - not in components. Plain functions for rules, a custom hook or store action to invoke them, components to render the result. The test is whether the rule is testable without rendering.
- “How would you explain React to someone from Angular or .NET?” - a custom
hook returning
{ data, actions }is a ViewModel; unidirectional flow replaces two-way binding. That vocabulary lands. - “Should we build an MVC layer on top of React?” - no, and the reason is that the idiomatic pieces already exist: server-state libraries for the model, stores for shared client state, hooks to package logic. Hand-rolling controllers re-creates the coupling React removed.