Skip to main content

MVVM architecture with Obsidian

MVVM (Model-View-ViewModel) is a software architecture widely used in modern front-end development. It emphasizes separation of concerns and improves code structure. In the Model-View-ViewModel pattern, code is organized into three layers:

  • The Model represents the data and business logic.
  • The View handles UI rendering and user interactions.
  • The ViewModel acts as an intermediary between the Model and the View. It exposes data and commands to the View, and allows for two-way communication between the Model and the View.

This decoupling results in code that's easier to test and maintain.

info

In this article you'll learn:

  1. How to structure a single UI component using MVVM architecture
  2. How to connect the layers together using Obsidian

Implementation

We'll refactor a basic counter application to use MVVM architecture. The application has a button that increments a counter, and an input field that displays the counter value. The original implementation uses hooks to manage the state and handle user interactions. Our aim in this refactor is to decouple the counter logic from the view into a separate class. For the sake of simplicity we chose a minimal example so we can focus on the architecture and the relationships between the layers.

This is the original implementation of the counter component:

Counter.tsx
const Counter = () => {
const [count, setCount] = useState(0);

return (
<div>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input readOnly type="text" value={count} />
</div>
);
};

Step 1: Create the model

The model represents the data and business logic of the application. In our case, the model has a single property count and a method increment() that increments the counter.

Notice that the count property is defined as an Observable. This is necessary to make the property reactive. The ViewModel will re-render the view whenever the count value changes.

CounterModel.ts
export class CounterModel {
public readonly count = new Observable(0);

public increment() {
this.count.value++;
}
}

Step 2: Create the ViewModel

The ViewModel mediates between the Model and the View. Unlike the model which contains data for an entire domain, the View Model contains a subset of that data that's relevant to a specific view. Our View Model exposes the current count value and the onIncrementClick method to the View.

CounterViewModel.ts
import { useObserver } from "react-obsidian"

export const useCounterViewModel = (model: CounterModel) => {
const [count] = useObserver(model.count);

return {
count,
onIncrementClick: () => model.increment(),
};
}

Step 3: Create the View

The view is responsible for rendering the UI. Let's refactor the original counter component to use the ViewModel. Our aim is to decouple the count logic from the view. It doesn't need to know how the count is incremented or where the count value comes from.

Counter.tsx
import {CounterGraph} from './CounterGraph';

// the viewModel hook is injected by the CounterGraph
const _Counter = ({useViewModel}: DependenciesOf<CounterGraph, 'useViewModel'>) => {
const {count, increment} = useViewModel();

return (
<div>
<button onClick={increment}>Increment</button>
<input readOnly type="text" value={vm.count} />
</div>
);
};

export const Counter = injectComponent(_Counter, CounterGraph);

Step 4: Create the Graph

At this point we have our Model, View, and View Model written. However, we still need to connect them to one another. We can do this by creating a graph.

CounterGraph.ts
import { Graph, ObjectGraph, Provides } from "react-obsidian";

@Singleton() @Graph()
export class CounterGraph {
@Provides()
model(): CounterModel {
return new CounterModel();
}

// The useViewModel hook is instantiated once and injected into the Counter component
@Provides()
useViewModel(model: CounterModel) {
return () => useCounterViewModel(model);
}
}

Wrapping up

We've successfully refactored the counter component to use MVVM architecture.

  • Our view is now decoupled from the counter logic,
  • The ViewModel exposes the data and commands to the view,
  • The Model contains reactive data and business logic.

We hope this article helped you understand how to use MVVM architecture with Obsidian. You can find a more fleshed out example in the obsidian-tic-tac-toe repository. If you have any questions or feedback, please reach out to us on Discord. We'd love to hear from you!