A view model is an object that encapsulates the state of the view. A "view model" is different from a "model." The model is the class that stores state that you normally save permanently in a database or file, while a view model stores the temporary state used while the application is running.
With a view model we can simply do all the complex logic in the model and then, using events, we can tell React that the view model has changed and it needs to update the component. A simple way to implement this would be to create our own Property class that can emit events to subscribed listeners, and then a custom hook called useProperty that calls React's useSyncExternalStore to synchronize the current component with the property of the view model.
export interface IReadOnlyProperty<T> {
// Gets the current value
get(): T;
// Returns a function to unsubscribe from events.
subscribe(callback: (value: T) => void): () => void;
}
Concrete Example
For example, let's say we have a list of items, and a component that lets us edit the name of the currently selected item. In order to save the edited name, you must press a save button. If you select a different item, the text box is supposed to reset to the name of the newly selected item.
Implementing this using only React's hooks is a genuine nightmare. That's because if you have a component that accepts an item as property, and you use useState(item.name) to keep track of the value on the textbox setting its initial value to the item.text attribute, you'll notice that if you change the item, the state doesn't reset.
export function ItemNameEdit(props: { item: Item }) {
// value is only set to item.text once.
const [value, setValue] = useState(props.item.text);
return <input value={value} onChange={(e) => setValue(e.target.value) }>;
}
One method to get around this situation is by tokenizing item and ignoring the state if we have the wrong token in the state.
export function ItemNameEdit(props: { item: Item }) {
let [value, setValue] = useState([props.item, props.item.text]);
if(value[0] !== props.item) {
value[1] = props.item.value;
}
return <input value={value[1]} onChange={(e) => setValue([props.item, e.target.value]) }>;
}
This works, but it looks hacky, and we will have to do it for every state that is supposed to reset.
Another way would be to simply not use item here, and let whoever changes the item also update the name.
export function ItemNameEdit(props: { value: string; setValue: (newValue: string) => void }) {
return <input value={value} onChange={(e) => props.setValue(e.target.value) }>;
}
This doesn't really solve the problem, it just moves the problem upward. We're going to have a list component, and that list component won't be a parent of this component, it will be a sibling or cousin, so the list needs to send a selection event up so that their common ancestor manages the state that it will send down.
This having to send data up just to it can go down is simply absurd. When I started using React, I had a lot of trouble making a simple form work because the state of the form was spread across several files.
The fundamental problem in this architecture is that we're using React as a framework. If we think of our application as React components, it will quickly turn into a nightmare, so let's not do that.
Instead, program view models first.
I don't have infinite lists of items, nor are there infinite boxes to set their names. It should be very easy to keep track of the lifetime of these components.
I can have an AppViewModel that acts as a global store of state, or, if you really want to, you can make it a React context so it's more modular. In this AppViewModel, I could have an ItemListViewModel that keeps track of what items are currently displayed, and what item is currently selected. And then, separately, I'd have an ItemNameEditViewModel for my ItemNameEdit component.
Done this, the code of the view becomes just:
export function ItemNameEdit(props: { viewModel: ItemNameEditViewModel }) {
const [value, setValue] = useProperty(viewModel.value);
return <input value={value} onChange={(e) => setValue(e.target.value) }>;
}
Now the view is as simple as it gets, and we can freely operate on the state of the application without having to care about the component hierarchy in React.
For example, if we want to update ItemNameEditViewModel.name when ItemListViewModel.currentItem changes, all we need to do is some good old event handling and data binding.
appViewModel.itemList.currentItem.subscribe((currentItem) => {
appViewModel.nameEdit.value.set(currentItem.name);
});
Lifetimes and Memory Leaks
In this architecture, the view models don't "know" about the views, but they naturally become tightly coupled to them. After all, the behavior of the application is programmed mainly in the view models. The hierarchy of React components reflects the variables you have in your AppViewModel. To add a new React component, you first need to add a new view model.
The reason for this is that if you don't program it this way, you end up with memory leaks in Javascript, which is the worst kind of memory leaks imaginable. Leaking memory in a garbage collected language is just shameful. How does this even happen?
That's because the main advantage of this architecture is that you can create view models that depend on other view models. For example, if a property changing in one view model affects another, you can simply make it a parameter in its constructor and then subscribe to its events. That works, but there is a problem: how do you unsubscribe later?
For example, if instead of <ItemNameEdit viewModel={appViewModel.nameEdit} /> we had viewModel={new ItemNameEditViewModel(appViewModel)}, we would have a memory leak. The constructor could be the one doing the data binding, which means a reference to the ItemNameEditViewModel would be contained inside the closure of the event handler that is added to currentItem.subscribe. We would need a destructor to remove the event handler, but managing these lifetimes from the React components would be extremely complicated. It's much simpler to manage these lifetimes from the view models themselves.
Thus, the components don't create new view models. The view models create new view models, and the components just use what already exists, so they don't need to call constructors, nor destructors, and if it's something is created or destroyed that's because a method of a view model was called and not because we added a new component.
This also means that in this architecture you don't really need to do prop drilling for view models. Since you only have one list in your application, you can simply get its view model from the appViewModel and pass it as the prop right when you need it.
const appViewModel = useContext(appViewModelContext);
return <ItemList viewModel={appViewModel.itemList} />;
With this you have a component that technically doesn't need to know about AppViewModel and works with any item list view model, even though your application only has one of them.
Separation of Concerns
It's important to note that in this architecture the view model doesn't depend on React.
This means that none of your view models should call any React hooks.
Because they don't call any hooks, you should be able to change any property from inside the web browser debugger without getting errors about React hooks being called outside of a component.
This might sound obvious to some, but I've searched for other people who tried an MVVM approach to Reach, and I found a lot of opinions I disagree with.
Some people even seem to think that React components themselves are view models. No. They are not. View models are view-agnostic, and React components are, by definition, coupled to React. I can't take a React component and use it as a view model for a vanilla Javascript front end, but I should be able to take an actual view model and do that.
Comparison with Redux
You may have heard about a another way to manage state in React called Redux, which is a library for React that provides some hooks to update a state, and you may be wondering what is the difference between using Redux to manage your React application state compared to using view models.
The key difference is that with Redux the application state becomes a single global object.
Being a single object makes updating state very easy in some cases. For example, you can literally object.apply a JSON that comes from a web server to control the entire state of your application. It's very easy for new developers to start programming an application if it uses Redux and they already know Redux because they will know where to get the state from. There are all sorts of tools that benefit from this standardized, centralized approach.
However, except for that, I don't really see any benefit.
To me, Redux feels way too complicated and the first time I saw them I thought reducers were some sort of elaborate joke. I mean, there is no way anyone is programming Javascript in 2025 by sending everything through a single function hat is just a immense switch statement, right? Of course, there is all sorts of techniques you can use to avoid the switch, but the architecture is still just a massive pipeline to a single centralized switch.
It's much easier to do myViewModel.func(a, b, c) than writing the equivalent dispatch. You can easily document what func does, and you can just as easily go to func's source code to see what it does.
One disadvantage of Redux compared to view models is that, because everything in Redux is in a single object, updating any single property updates the entire state, and every time the state object changes identity, React has to do the whole rendering process again, which can become impractical when the updates are very frequent and the components are sufficiently complex. This can happen, for example, if your state includes the current value of a text box currently being edited, but that state is also used by everything in the entire application.
This doesn't happen with the view model approach. With a view model, you simply subscribe to a single property of the view model object, and the component only needs to be rendered when that property updates. Changing the property doesn't require checking for changes in other components that didn't subscribe to that specific property. Every component only subscribes to the properties they care about.