Frontend / React / 28_mvc_pattern.md

MVC, and what React actually is

Updated 4 interview angles 5 min read source
On this page7
  1. MVC in one diagram
  2. Why “React is the V” is the right first sentence
  3. Why it is also incomplete
  4. What people mean when they build “MVC in React”
  5. Where it breaks in production
  6. Related
  7. Interview angle

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

text
   user ──▶ Controller ──▶ Model ──▶ View ──▶ user
              │  decides      │ owns     │ renders
              │  what         │ data     │ what the
              └─ changes      └─ rules   └─ model says

Model 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.

tsx
// 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.
tsx
// 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.

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.