The MVC Pattern Still Hides in Every Modern Framework

A junior engineer on my team once told me, with some pride, that our codebase did not use MVC. “We just use hooks and components,” he said. Twenty minutes later we were reviewing his pull request: one component file, six hundred lines, doing data fetching at the top, holding four pieces of state, deriving two values with `useMemo`, registering an effect to sync something with the URL, and then returning markup at the bottom. I pointed at the middle third and said that it was a controller with the view pasted onto it. He stared at the screen for a while. So did I, honestly, because he had written it exactly the way the framework’s tutorials told him to.

Both things were true. The team had no files named controller, and the application had controllers in everything but name.

The boxes keep coming back

Cardboard boxes labeled keep, donate, and trash for effective home organization.
Photo by RDNE Stock project on Pexels

The model, view, and controller split was documented at Xerox PARC in 1979, in a Smalltalk environment, as a way to keep the data, the presentation, and the coordination from smothering each other. Rails made the vocabulary universal on the web in 2004. Backbone, Angular, and Ember brought it to the browser around 2010 to 2013, complete with routers and models you could name in stand-up. Then the component era arrived and the acronyms fell out of fashion. React’s rise came with a genuine argument that the old division was the wrong shape for UI work, and teams listened.

The boxes came back anyway, because they encode a real constraint. Business state changes for business reasons. Rendering changes for presentation reasons. The glue between them changes whenever a user touches something. Separate those and your codebase stays navigable. Mix them and every review turns into a game of finding where a given value is actually being decided.

Look at a modern app honestly and you can point at all three. The state layer is the model: a store, an atom, a set of signals, a query cache that holds server data and its freshness. The component templates and render functions are the view. The handlers, reducers, route loaders, effects, and server actions coordinate the two, and that is the controller wearing whatever the current framework calls it. In some stacks the controller code has moved to the server, in loaders and actions that fetch data and return it to the component. It still decides. It just decides on the other side of the network.

Why frameworks hide it

A man in a sweater examines a rustic hut in a field of tall grass and reeds.
Photo by Jimmy Boos on Pexels

Renaming has real advantages. When the controller lives inside the component, you can put related code in one place instead of hunting through a directory tree. Hooks made side effects declarative and cut out a generation of lifecycle bugs. Signals and derived values make the view update itself without hand-written coordination code. Each rename bought something concrete.

The hiding still has costs. The first one shows up in review: without names for the roles, every file gets evaluated from scratch. I have watched teams spend twenty minutes discovering that a component fetches its own data, owns state that two siblings also own, and formats currency. The fix was obvious once someone said the words “this does model and controller work” out loud. Vocabulary is a compression format for code review.

The second cost is state ownership. If everything is a component with hooks, the question “where does this value live?” gets answered by whichever component grabbed it first, and two months later it is answered three different ways. Frameworks keep inventing new homes for shared state (contexts, stores, atoms, query caches) because the question is unavoidable. Naming the model layer explicitly in your own code, even as folders or conventions, keeps the question cheap to answer.

The third cost is tooling. Good development tooling has always been built around the split: one panel for state changes, one for the rendered tree, one for the stream of events between them. When your architecture respects the boxes, the tools show you the whole loop: state mutated, event dispatched, view re-rendered. When it scrambles them, the panels show you fragments and debugging becomes archaeology.

Where the pattern genuinely runs out

Tracing pattern sewing (cropped) notches
Photo by MissMessie on Wikimedia Commons

There are systems where the division hurts. Real-time collaborative editing, local-first data with conflict resolution, and game loops all entangle simulation state and presentation on purpose, and forcing MVC on them creates ceremony. Those are also the hard, interesting minority of the work. For the CRUD app that 95% of us ship, the boxes fit because the problem really does have data, presentation, and coordination in it.

Making it work for you

You do not have to rename your files. My current team keeps components, hooks, and stores, and we simply agree on the roles during review: state decisions get made in one layer, side effects get their own place, and components render. When someone pastes all three into one file, the review comment is about the role that leaked, and everyone knows what that means.

The 1979 diagram describes how change wants to be organized in interactive software, and it has survived every naming scheme thrown at it. When you see the boxes in your stack, naming them makes you faster. When a vendor or a trend tells you the boxes are gone this time, review the code in front of you and see for yourself.

Dawn Rose

Back to top