MVVM — Model View ViewModel

The State Synchronisation Problem

When I added real-time features to the POS dashboard — order status updating live via WebSocket — I realised my old approach of manually wiring DOM updates was not going to scale. Each status change required finding the right element, checking if the component was still mounted, updating text, and toggling CSS classes. Every new piece of reactive data added another branch to maintain.

MVVM solves by making state the single source of truth. The ViewModel holds observable state. The View binds to that state and re-renders automatically whenever it changes. I do not imperatively update the DOM — I update data, and the view follows.

Table of Contents


The Three Roles

┌───────────────────────────────────────────────────────────┐
│                           View                            │
│  - Renders bound ViewModel state                          │
│  - Triggers ViewModel actions on user events              │
│  - No business logic, no direct model access              │
└───────────────────────────┬───────────────────────────────┘
          binds to / calls  │          observable state

┌───────────────────────────────────────────────────────────┐
│                        ViewModel                          │
│  - Observable state (reactive properties)                 │
│  - Exposes commands (actions/methods for the view)        │
│  - Transforms model data for display                      │
│  - Orchestrates async operations                          │
└───────────────────────────┬───────────────────────────────┘
            reads / updates │

┌───────────────────────────────────────────────────────────┐
│                          Model                            │
│  - Data, domain rules, persistence, API services          │
│  - No knowledge of ViewModel or View                      │
└───────────────────────────────────────────────────────────┘

The key difference from MVC and MVP: the View and ViewModel are linked by two-way data binding (or one-way data flow in React). The ViewModel never calls the View directly — it just updates state and the View reacts.


Data Binding: The Core Idea

In React, this binding mechanism is useState / useReducer / external store (Zustand, MobX). In Vue it is ref / reactive. In WPF it is INotifyPropertyChanged. The mechanism differs; the concept is the same.


TypeScript + React Example: Order Dashboard ViewModel

The View component contains no logic. It only reads ViewModel state and calls ViewModel actions. All data transformation (toRow) and state management lives in the ViewModel hook.


Async Operations in the ViewModel

The ViewModel is the right place to manage loading and error state for async operations. A pattern I use consistently:

Each async operation in the ViewModel transitions through these states. The View renders based on which state is active — no if-else chains guarding on multiple boolean flags.


ViewModel Testing

Because the ViewModel is a React hook, test it with @testing-library/react-hooks (or renderHook from @testing-library/react):


MVVM vs MVP vs MVC

Concern
MVC
MVP
MVVM

View updates

Controller selects view

Presenter calls view methods

View binds to ViewModel state

View knowledge

May reference model data

Only knows presenter

Only knows ViewModel state

View passivity

Moderate

High (passive)

High (declarative bindings)

Testability

Needs HTTP / context

Presenter: pure class

ViewModel: hook/class test

Best fit

Server-side MVC, REST APIs

Android, testable desktop UIs

React, Vue, reactive frontends

Two-way data binding

No

No

Yes (or reactive one-way flow)


When to Use MVVM

MVVM is the natural fit when:

  • Using a reactive UI framework (React, Vue, Angular, Svelte, SwiftUI)

  • The UI has complex state — loading states, multiple derived values, optimistic updates

  • Reusing the same ViewModel across different Views (desktop + mobile, different themes)

  • The team is larger and View/ViewModel separation enforces discipline


Lessons Learned

  • The ViewModel should never import React DOM or component types. If it does, it will not be portable or testable in isolation.

  • Transform data in the ViewModel, not in the View. totalFormatted, statusColor, canVoid — all of these belong in the ViewModel so the View stays declarative.

  • One ViewModel per screen or feature area, not per component. Sharing a ViewModel across components that belong to the same screen is fine; sharing it across unrelated screens is not.

  • Prefer one-way data flow in React. React does not have two-way binding by default. MVVM in React means: state flows down from ViewModel to View, events flow up from View to ViewModel. Do not fight the framework.

Last updated