Level up your React architecture with MVVM

Danijel Vincijanovic
COBE
Published in
7 min readAug 30, 2018

--

Have you ever opened a project and left traumatised because you saw an incomprehensible and unmaintainable JavaScript code that you don’t want to touch even with an isolated rod? Because if you touch it, everything could crash just like one big Jenga block.

Developer vs unmaintainable code

JavaScript is easy to pick up and start with coding but it is even easier to do it in a wrong way. For small projects, low quality code will not present high risk for a company but if a project gets bigger you will end up with a technical debt that will wade every deadline and eventually swallow you up. No one would ever want to touch this kind of code. Therefore, in this article we’ll see how you can apply Model-View-ViewModel (MVVM) architectural pattern into a React project and significantly improve the code quality.

By definition, an Architecture Pattern provides a set of predefined subsystems, specifies their responsibilities, and includes rules and guidelines for organising the relationship between them.

Many architectural patterns are trying to solve the same challenges as MVVM — make your code loosely coupled, maintainable and easy to test.

Someone could ask: “If I already know how to use Flux and Redux, why should I bother myself learning MVVM or any other architectural pattern?”
and the answer is: you don’t have to! If, for example, Redux is ideal fit for your project and your team, stick with it. On the other hand, how could you be 100% sure that Redux is truly ideal for your project if you don’t know any other pattern? You’ll force Redux into every project even though there might be a better option. Only sane decision here is to learn new architectural pattern. Let’s start with the MVVM.

The best way to get to know patterns is to get your hands dirty and try them out. We’ll create Pokédex demo application powered with React and MobX (complete code in this repo). MobX is a library for simple and scalable state management. It’s serving the same purpose as Redux, but unlike Redux it doesn’t give us guidelines how we should architect our application. MobX gives us observable capabilities (observer pattern) and a way to inject dependencies into our components. It goes with MVVM just as bread goes with the butter.

Demo application that we’re gonna build

Dive into MVVM

MVVM has four main blocks:

  • The View — UI layer that users interact with,
  • The ViewController —has access to the ViewModel and handles user input,
  • The ViewModel — has access to the Model and handles business logic,
  • The Model — application data source

Keep reading to find out how those components in MVVM is related to each other and what are their responsibilities.

MVVM diagram

View

With React we’re building user interfaces and this is what most of us are already familiar with. The View is the only touching point for a user with your application. A user will interact with your View that will trigger ViewController methods depending on events such as mouse movements, key presses etc. The View is not only used for a user input but also for displaying output — results of some actions.
The View is a dumb, presentational React.Component which means that it should be used only for displaying data and triggering events passed from the ViewController. This way, we’re keeping our components reusable and easy to test. With the help of MobX, we’ll turn React.Componentinto reactive component that will observe any changes and automatically update itself accordingly.

import React from 'react'
import PokemonList from './UI/PokemonList'
import PokemonForm from './UI/PokemonForm'

class PokemonView extends React.Component {
render() {
const {
pokemons,
pokemonImage,
pokemonName,
randomizePokemon,
setPokemonName,
addPokemon,
removePokemon,
shouldDisableSubmit
} = this.props

return (
<React.Fragment>
<PokemonForm
image={pokemonImage}
onInputChange={setPokemonName}
inputValue={pokemonName}
randomize={randomizePokemon}
onSubmit={addPokemon}
shouldDisableSubmit={shouldDisableSubmit}
/>
<PokemonList
removePokemon={removePokemon}
pokemons={pokemons}
/>
</React.Fragment>
)
}
}

export default PokemonView

Note: PokemonList component is decorated with @observer decorator instead of using regular function observer(class PokemonList {...})
Decorators are not supported by default so if you want to use them you’ll need babel plugin.

ViewController

The ViewController is a brain for the View — it has all View related logic and owns a reference to the ViewModel. The View is not aware of the ViewModel and it is relying on the ViewController to pass all necessary data and events.
Relation between the ViewController and the ViewModel is one-to-many — one ViewController can have references to different ViewModels.
Handling user input shouldn’t be left to the ViewModel but rather handled in the ViewController that will pass clean and prepared data to the ViewModel.

import React from 'react'
import PokemonView from './PokemonView'

class PokemonController extends React.Component {
state = {
pokemonImage: '1.gif',
pokemonName: ''
}

setRandomPokemonImage = () => {
const rand = Math.ceil(Math.random() * 10)
this.setState({ pokemonImage: `${rand}.gif` })
}

setPokemonName = (e) => {
this.setState({ pokemonName: e.target.value })
}

clearPokemonName() {
this.setState({ pokemonName: '' })
}

savePokemon = () => {
this.props.viewModel.addPokemon({
image: this.state.pokemonImage,
name: this.state.pokemonName
})
}

addPokemon = () => {
this.savePokemon()
this.clearPokemonName()
}

removePokemon = (pokemon) => {
this.props.viewModel.removePokemon(pokemon)
}

render() {
const { viewModel } = this.props

return (
<PokemonView
pokemons={viewModel.getPokemons()}
pokemonImage={this.state.pokemonImage}
randomizePokemon={this.setRandomPokemonImage}
setPokemonName={this.setPokemonName}
addPokemon={this.addPokemon}
removePokemon={this.removePokemon}
pokemonName={this.state.pokemonName}
shouldDisableSubmit={!this.state.pokemonName}
/>
)
}
}

export default PokemonController

ViewModel

The ViewModel is a producer who doesn’t care who consumes data; it can be React component, Vue component, aeroplane or even a cow, it simply doesn’t care. Because the ViewModel is just a regular JavaScript class it can be easily reused anywhere with UI tailored differently. Every dependency needed by the ViewModel will be injected through the constructor, thus making it easy to test. The ViewModel is interacting directly with the Model and whenever the ViewModel updates it, all changes will be automatically reflected back to the View.

class PokemonViewModel {
constructor(pokemonStore) {
this.store = pokemonStore
}

getPokemons() {
return this.store.getPokemons()
}

addPokemon(pokemon) {
this.store.addPokemon(pokemon)
}

removePokemon(pokemon) {
this.store.removePokemon(pokemon)
}
}

export default PokemonViewModel

Model

The Model is acting as a data source ie. global store for the application. It composes all data from the network layer, databases, services and serve them in a easy way. It shouldn’t have any other logic except one that actually updates a model and doesn’t have any side effects.

import { observable, action } from 'mobx'
import uuid from 'uuid/v4'

class PokemonModel {
@observable pokemons = []

@action addPokemon(pokemon) {
this.pokemons.push({
id: uuid(),
...pokemon
})
}

@action removePokemon(pokemon) {
this.pokemons.remove(pokemon)
}

@action clearAll() {
this.pokemons.clear()
}

getPokemons() {
return this.pokemons
}
}

export default PokemonModel

Note: in the code snippet above, we’re using @observable decorator on every property that will be observed by the View. Any piece of code in the Model that updates some observable value should be decorated with @action decorator.

Provider

One component that is not part of the MVVM but we’ll use it to glue everything together is called Provider. This component will instantiate ViewModel and provide all needed dependency to it. Furthermore, instance of the ViewModel is passed through props to the ViewController component.
Provider should be clean, without any logic as its purpose is just to wire up everything.

import React from 'react'
import { inject } from 'mobx-react'
import PokemonController from './PokemonController'
import PokemonViewModel from './PokemonViewModel'
import RootStore from '../../models/RootStore'

@inject(RootStore.type.POKEMON_MODEL)
class PokemonProvider extends React.Component {
constructor(props) {
super(props)
const pokemonModel = props[RootStore.type.POKEMON_MODEL]
this.viewModel = new PokemonViewModel(pokemonModel)
}

render() {
return (
<PokemonController viewModel={this.viewModel}/>
)
}
}

export default PokemonProvider

Note: in the code snippet above, @inject decorator is used to inject all needed dependencies into Provider props.

Recap

If you‘re not familiar with your toolbox then you might have a problem; as you will probably try to solve every task with the same tool. A sledgehammer is a good option for cracking nuts but maybe its not the best one — different tasks require different set of tools! You don’t have to be the master of them all but you need to know what you have in your toolbox so you can choose more wisely. Maybe your current architecture is like a plastic sledgehammer for cracking nuts but MVVM could be a real nutcracker. With MVVM you can achieve a clean separation of concerns and testing will become like a summer breeze.

Clone this Github repo if you want to play with Pokedéx application and check these articles if you want to learn more about MVVM and architecture in general:

Danijel is a web developer at COBE. JavaScript warrior with React as shiny dagger and Node as blazing shotgun ready to fire. In his spare time, like every other geeky kid, he likes to create pens on Codepen and develop tiny Node apps while listening to RHCP in the background thread. Apart from his JavaScript warrior dreams, he likes to lift weights, eat a lot of chicken and watch crime/thriller movies till the late hours.

--

--