sleepy

TOPICS

IntroductionReact (with MobX)ArchitectureModelsNetworkingData TransformersStoresViews & View Models

MVVM

What is MVVM?

MVVM is a pattern you can apply to your UI in order to easily and write tests on your UI logic without needing to write UI tests. Move your logic out of your UI and simplify your testing to reduce headaches for your future self.

This documentation will attempt to show you real world, full app examples of MVVM, why it's useful, how to write tests, and what it looks like in React (using MobX), iOS (using Combine), and Android. In order to have good MVVM practices, we also need to have good architecture, so the other sections of this documentation will go over that as well.

The Counter Example

If you've read the React documentation for using Hooks, you'll be familiar with this example. We'll use their counter example to show how it would be written with MVVM.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React, { useState, useEffect } from "react"; function Example() { const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }

With MVVM, it looks like this

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { makeAutoObservable } from "mobx"; import { observer } from "mobx-react"; interface Props { viewModel: ViewModel; } const View = observer(( { viewModel }: Props // the observer wrapper is specific to MobX, not MVVM ) => ( <div> <p>{viewModel.countLabel}</p> <button onClick={viewModel.onClick}>Click me</button> </div> )); class ViewModel { private count = 0; private document: Document; constructor(document: Document) { makeAutoObservable(this); // Specific to MobX, not MVVM this.document = document; this.document.title = `You clicked ${this.count} times`; } onClick = (): void => { this.count += 1; }; get countLabel(): string { const label = `You clicked ${this.count} times`; this.document.title = label; return label; } } const Example = () => { const viewModel = new ViewModel(document); return <View viewModel={viewModel} />; }; export default Example;

So what happened here? We took all of the "business logic" and moved it out of the UI (insert Patrick meme) and into a class called ViewModel (note, I know some folks don't like classes, feel free to use whatever object type you prefer that can hold state). The ViewModel is now pure logic and the View is now pure UI with injected props. Each of these pieces now has a single responsibility. The Example is what we call the "blinder" or the "glue" in MVVM that connects the two together. // CMB: Perhaps fill in some more here about the View always being wrapped in the observer and never the binder

Tests

And now to the good stuff and the whole reason we're doing this. Tests! 😄

We'll first start off again with the equivalent example from the React Hooks documentation.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import React from "react"; import ReactDOM from "react-dom"; import { act } from "react-dom/test-utils"; import Counter from "./Counter"; let container; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { document.body.removeChild(container); container = null; }); it("can render and update a counter", () => { // Test first render and effect act(() => { ReactDOM.render(<Counter />, container); }); const button = container.querySelector("button"); const label = container.querySelector("p"); expect(label.textContent).toBe("You clicked 0 times"); expect(document.title).toBe("You clicked 0 times"); // Test second render and effect act(() => { button.dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(label.textContent).toBe("You clicked 1 times"); expect(document.title).toBe("You clicked 1 times"); });

Because the component is the markdown that drives the UI, this is essentially a UI test that also tests the logic. That can start to get quite complex as our components and their state grow in size. So how would we test this counter logic with MVVM?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 let viewModel: ViewModel; let mockDocument: Document; beforeEach(() => { // @ts-expect-error We aren't mocking the entire Document, only the part we need mockDocument = { title: "" }; viewModel = new ViewModel(mockDocument); }); it("has initial labels", () => { expect(viewModel.countLabel).toEqual("You clicked 0 times"); expect(mockDocument.title).toEqual("You clicked 0 times"); }); describe("when clicked", () => { beforeEach(() => { viewModel.onClick(); }); it("can update the counter labels", () => { expect(viewModel.countLabel).toEqual("You clicked 1 times"); expect(mockDocument.title).toEqual("You clicked 1 times"); }); });

This ViewModel has 100% test coverage with valid, useful tests! We don't need to search for buttons or labels or get bogged down by containers and query selectors. Because our logic is completely separated from the UI, every line in our tests is simple and meaningful.

What is (not) MVVM?

  • A framework/library/dependency
  • A platform specific pattern
  • A single architecture for your entire app (you still need to have a plan for organizing the rest of your non UI logic, networking or other dependencies, etc)

What is MVVM?

A way to set up your UI so that you can remove state and logic from your UI views and keep them “dumb”.

The simplest representation of what MVVM is the following:

We should take our UI specific logic…. AND PUSH IT SOMEWHERE ELSE

What is the purpose?

Testing.

Being able to write simple tests on only the code the actually matters (our business logic, the logic we are writing ourselves), we're able to be confident our code is working as expected, we're able to lock in and document business requirements with our tests, we can easily cover non happy paths, and we keep our code decoupled. Our future selves will be grateful.

Who is this for?

You may find this useful if you...

  • are interested in easily unit testing your view components.
  • want to learn more about general testing fundamentals. My favorite part about these patterns is they are library and framework agnostic! We separate our code by responsibility, use interfaces and inject mocks, and don't need to spin up mock servers or mock apps.
  • think tests slow you down or are useless but think you might change your mind. Writing tests on non testable code is frustrating and sometimes impossible. If you think you can see the value of tests but have never had any good experiences with writing tests yourself, hopefully you can learn something here.
  • enjoy learning different ways to write clean and testable code.

What's Next?

There is a LOT here we haven't gone over. The purpose of this introduction was to show a simple example most folks may already be used to and how it can translate direction to MVVM by completely separating UI from logic. If you're still interested, if you'd like to see how it works in more complicated examples, why we injected a Document, want to learn about the observable pattern and why we are using MobX, then let's dive in!