DEV Community

Cover image for How To Use MVVM in React Using Hooks and TypeScript
Dennis Persson
Dennis Persson

Posted on • Originally published at perssondennis.com

How To Use MVVM in React Using Hooks and TypeScript

Software applications often start out simple and quickly grows big. No matter how small one try to keep it, the code somehow always grows up to be a full-fledged application. In many languages, we follow structural patterns as MVVM and Clean Architecture, which is less common to see in the Wild West Web.

In this article, we will take a look at how a React application can be designed to follow the MVVM architectural design pattern to create a React application that is both scalable and maintainable. We will use TypeScript and functional React components and write custom hooks for the ViewModel and Model. The same code will of course work with React Native with some minor modifications.

If you have some questions while reading the article you have an extensive FAQ at the end of the article.

In This Article

MVVM Overview

If you don't know what MVVM is, you can either google it or read along anyway. You should be able to follow along in the article either way. In short, MVVM is a design pattern that provides a clear separation of concerns, making it easier to develop, test, and maintain software applications.

What MVVM does is to separate an application into Views, ViewModels and Models. Where the Views are responsible for presenting data to the user and capturing user input, the ViewModels act as a mediator between the Views and the Models, and the Models manage the application data.

Architecture meme
Some people say architecture matters

Application Overview

The app we will build in this article is the tiny one you can see down here. It's kept small so we can focus on the MVVM architecture. The app consists of a single View listing articles, with an input field for adding a new article with a given name.

React MVVM example app
The app to be built, a single View to keep it simple

Codewise, the application consists of one React component for the articles View, then a few hooks for the ViewController, ViewModel and Model. Below is a architectural diagram of the code we will go through. Disregard the ViewController for now, which isn't an official part of MVVM, we will discuss that one later.

React MVVM app overview
Overview of the React components for the application in this article

A single View is of course not enough for a useful application. If the example would be larger, each View would have its own ViewController, as depicted in the diagram below. All ViewControllers that work against the same Model would use a common ViewModel.

React MVVM app extended overview
What the application would look like with more views

When the application grows even more it can include more Models and ViewModels. I think you get that. Anyhow, let us start looking at the implementation for the app.

View Implementation

A View in the application can be any screen or visual component on the screen. If the site to be built is a blog platform, we could have screens for listing articles, viewing a single article, and for writing a new article. Those screens could be implemented with in Views like ArticlesView, ArticleView and CreateArticleView

The View is responsible for displaying the UI and trigger actions based on user interactions. Although there traditionally isn't a ViewController in MVVM, this MVVM guide has split the View part of MVVM into two parts, a View and a ViewController, where the only responsibility of the View component is to render the UI while the ViewController handles all the logic for the View.

View Component

As we saw before, the View will will look something like this with some CSS.

ArticlesView component
ArticlesView component

To implement the View, we use a regular functional React component that renders a UI. To avoid handling view logic in the View component, we use a ViewController which exports functions for all possible user actions.

// view/article/articles-view/ArticlesView.tsx

import React from 'react'
import useArticlesViewController from 'view/article/articles-view/useArticlesViewController'

const ArticlesView: React.FC = () => {
  const {
    articleName,
    articles,
    navigateToArticle,
    onArticleNameChange,
    onCreateArticleClick,
  } = useArticlesViewController()

  return (
    <div>
      {!!articles &&
        articles.map(({ id, title }) => (
          <div key={id} onClick={() => navigateToArticle(id)}>
            {title}
          </div>
        ))}
      <div>
        <input
          type="text"
          onChange={onArticleNameChange}
          value={articleName}
          placeholder="Add an article..."
        />
        <button onClick={onCreateArticleClick}>Create article</button>
      </div>
    </div>
  )
}

export default ArticlesView
Enter fullscreen mode Exit fullscreen mode

The ArticlesView component above renders a view with articles. When the user interacts with the UI, this View component will simply forwards the action to a function in the useArticlesViewController hook.

Since this file only contains pure UI components and only reacts to user interactions by forwarding them to a hook, it's very trivial to test and read. The unit tests for this component would merely be to test that all articles are rendered and that the functions exported from the useArticlesViewController are called. The useArticlesViewController itself would preferably be mocked since we will test that hook separately.

To test interactions with the view, I would recommend interactional testing frameworks like React Testing Library. You can read more about the benefits and how that compares to code based unit testing in my other article about unit testing

In love with css meme
I won't bother you with the CSS

ViewController Hook

The ViewController hook used in the View component handles the view logic for that View. Each View component has its own ViewController hook. There are several types of view logic in the ViewController.

  • View logic that updates the View components internal state (typically useState, useReducer or useRef)
  • View logic that updates the application's state (e.g. navigating to other screens)
  • View logic that interacts with the ViewModel

In general, the View logic is responsible for managing the user interface and handling user interactions. This includes maintaining the component's internal state as well as handling conditions for interacting with the ViewModel. By keeping all this kind of view logic in the ViewController, we don't need any view logic in the ViewModel.

// view/article/articles-view/useArticlesViewController.tsx

import { useCallback, useEffect, useState } from 'react'
import { useHistory } from 'react-router-dom'
import { HOME_SCREEN, ROOM_SCREEN } from 'routing/screens'
import useArticleViewModel from 'viewmodel/useArticleViewModel'

const useArticlesViewController = () => {
  const history = useHistory()
  const [articleName, setArticleName] = useState('')
  const { articles, createArticle, getArticles } = useArticleViewModel()

  const onCreateArticleClick = useCallback(async () => {
    await createArticle({ name: articleName })
  }, [createArticle, articleName])

  const navigateToArticle = useCallback((articleId: number) => {
    history.push(`${ROOM_SCREEN}/${articleId}`)
  }, [history])

  useEffect(() => {
    getArticles()
  }, [getArticles])

  return {
    articleName,
    articles,
    navigateToHome,
    navigateToArticle,
    onCreateArticleClick,
    onArticleNameChange: setArticleName
  }
}

export default useArticlesViewController
Enter fullscreen mode Exit fullscreen mode

useArticlesViewController is a little more complex than its corresponding View component. It contains code for navigation among screens using React Router. It also keeps the internal state of the view, which can be used for both handling internal logical conditions or to be passed back to the View component to be used in rendering.

Lastly, we use a useArticleViewModel hook which is the ViewModel that this View uses. Only the functions required to be used in this View needs to be destructured from that ViewModel.

This file includes more logic to test than the View component. The good thing is that there's no need to test any UI elements at all. You can test the hook purely using React Hooks Testing Library, there's no need to use any UI element matchers or to check if data is rendered. Simply invoke the functions returned from the hook and assert the expected outcome.

ViewModel Implementation

The ViewModel is responsible for handling the interactions between the View and the Model, serving as a bridge between them. Normally, the View interacts directly with the ViewModel, but as we already have seen, we have split the View into a View component and a ViewController. This means the ViewController is the one which uses the ViewModel.

The ViewModel is, just as the ViewController, implemented as a hook. It doesn't include very much code in our Article example. This is because our application example simply doesn't have any business logic. But if we would have it, we would have put it here.

// viewmodel/useArticleViewModel.tsx

import useArticleModel from 'model/useArticleModel'

const useArticleViewModel = () => {
  const { article, articles, createArticle, getArticles } = useArticleModel()

  return {
    article,
    articles,
    createArticle,
    getArticles
  }
}

export default useArticleViewModel
Enter fullscreen mode Exit fullscreen mode

Yeah, that's it. It only wraps the functions in the useArticleModel hook by re-exporting them. If some of those actions would have required any business logic, we would have created a wrapper function to perform the expected calculations within this hook.

What about testing? Well, when looking at this I would argue there's no need to test this file. With more logic, you can use React Hooks Testing Library here as well.

Testing is important meme
I didn't use to think testing is important, but somehow I changed my mind...

Model Implementation

The model is responsible for getting data from data sources, and serving it to the ViewModel. The ViewModel in turn, uses the Model to fetch and modify the data.

The model can be written in a plenty of ways. Redux, Apollo, hooks like useSWR and so on can all be used. Furthermore, there may not only be a single data source, complex applications can fetch data from several different sources. In that case, it can be a good idea to put a repository in between the ViewModel and the Model. Repositories are not a part of MVVM design pattern and will therefore not be used here.

Because the implementation of the model can vary significantly between projects, this guide will not delve deeply into how to implement it. Instead, we will provide a simple example that makes use of REST API functions for retrieving and posting articles.

// model/useArticleModel.tsx

import { useCallback, useState } from 'react'
import { getAllArticles, postArticle } from 'model/api/article'
import { ArticleDTO, CreateArticleDTO } from 'model/api/article'

const useArticleModel = () => {
  const [articlesData, setArticlesData] = useState<ArticleDTO[] | null>(null)

  const getArticles = useCallback(async () => {
    const articles = await getAllArticles()
    setArticlesData(articles)
  }, [])

  const createArticle = useCallback(async (createData: CreateArticleDTO) => {
    if (Array.isArray(articlesData)) {
      const response = await postArticle(createData)

      if (response !== null) {
        setArticlesData([...articlesData, { id: response.id, name: response.name }])
      }
    }
  }, [articlesData])

  return {
    articles: articlesData,
    createArticle,
    getArticles
  }
}

export default useArticleModel
Enter fullscreen mode Exit fullscreen mode

As you can see, there's more logic in this one. Still we have kept it brief. The logic is not very smart or robust, but we can see that we have basic functionality for fetching and creating articles. The code should be fairly easily tested in a similar way as the previous hooks.

REST API

In the Model example above, we didn't get to see how the REST API functions were implemented. Even though it isn't necessary for the MVVM example, we will still look at some trivial example code for what it can look like.

This code will most likely not look the same in your project, it is simply here to serve an example. Your real code should hopefully be better written than this, typically you would need to handle things like errors, retires, caching and to make the code idempotent.

// model/api/article.ts

export interface ArticleDTO {
  id: number
  name: string
}

export interface CreateArticleDTO {
  name: string
}

const API_URL = 'https://example.com/api'

export const getAllArticles = async (): Promise<ArticleDTO[] | null> => {
  const response = await fetch(`${API_URL}/articles`)
  if (!response.ok) {
    throw new Error('Failed to get all articles')
  }
  const data = await response.json()
  return data
}

export const postArticle = async (createData: CreateArticleDTO): Promise<ArticleDTO | null> => {
  const response = await fetch(`${API_URL}/articles`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(createData)
  })
  if (!response.ok) {
    throw new Error('Failed to create article')
  }
  const data = await response.json()
  return data
}
Enter fullscreen mode Exit fullscreen mode

FAQ

Q: What is a View?

A View is a substantial part of the UI. It can be the complete viewport or it can be a component within the viewport which should handle its own state and its own data. I don't know in which way you have worked with container and components in React, or smart and dumb components or whatever you call them. But if you think of a component as a dumb component, that is not a View, that's just a part of a View. Buttons, input field and small components are all components, not Views.

Q: Why is there a ViewController in the MVVM implementation?

The ViewController is added as a part of the View to split the user interface implementation from its logic. This allows the View to be tested more easily since the view logic and UI can be tested separately. The downside is that we get more components to test, render and maintain.

Another reason to add this extra layer, is to serve as a bridge between the View and the ViewModel. By doing this, we can keep more of the view logic inside the View layer instead of spilling it over to the ViewModel which is shared by multiple Views. There's no need for a ViewModel to know about the internal state's of the View that utilizes it.

Q: Do I really need a ViewController for my each View component?

That depends on what you think of when you say View. Read the question What is a View?. If the component you are writing isn't considered to be a View, you won't need, and neither shouldn't, write a ViewController for it. It is definitely overkill to add a ViewController for a trivial button component. If the component you implement really is a View, I would suggest adding its view logic to a ViewController for testing purpose and for being consistent with how you implement you Views.

Q: How to implement MVVM in React without a ViewController?

If you don't want to split the View into a View and ViewController, simply put all of the view logic in the ViewController into the View component itself. The other option would be to put parts or all of that logic in the ViewModel, but then the ViewModel would contain a lot of view logic that is specific to certain Views, which would make it confusing and quite huge.

Q: Why do we use hooks to implement the ViewController?

The ViewController could be implemented in multiple ways. We could for example implement the View and ViewController as two components, where the ViewController component would be the main component which simply passes props to the View component. That solution would increase the virtual and possible even physical DOM since it would require two components instead of one.

Q: Can we implement MVVM in React using old class components?

Yes, of course. But in this article we use functional components. Class components are old and should not be used anymore.

Q: I have seen MVVM being implemented using dependency injection in React.

Good for you. I have seen class components using that approach, which suits well there. Implementing MVVM in React with functional components comes with other solutions. Hooks are more familiar to React developers than dependency injection patterns. For that reason, this example uses React hooks to implement MVVM.

Q: Should my React MVVM application only include a single Model?

Probably not. You will likely have multiple models in your application. Each model will also come with a ViewModel which utilizes the Model.

Q: Should my React MVVM application only include a single ViewModel?

No. Each Model will have a ViewModel. If you have multiple Models you may also have multiple ViewModels.

Q: Should I have one or several ViewController for each ViewModel?

You should have one ViewController for each View. Several ViewControllers will use the same ViewModel. ViewControllers like useArticleViewController, useArticlesViewController and useCreateArticleViewController will all share the same ArticleViewModel.

Q: Can I only use a single ViewModel in a ViewController?

No, you can use many. The ViewController-ViewModel relation is a many-to-many relation, not a many-to-one relation. A View can use data from multiple models, and will in that case use multiple ViewModels. For example, a useArticleViewController, could utilize both a ArticleViewModel and a AuthorViewModel.

Q: Why do you wrap createArticle function in onCreateArticleClick in the ViewController?

This is done for two reasons. First reason is just for naming conventions, to keep view logic such as clicks handling in the ViewController rather than in the ViewModel. The second reason is because the ViewController has access to the internal state of the View. That makes it possible to update onCreateArticleClick with logic from the view, such as text from input fields.

Q: I have a component which doesn't need the Model do I still need use a ViewModel in the ViewController?

If I would answer yes, what would you write there? It's totally fine and even normal to have components without ViewModels.

Q: Do I still need testing libraries like React Test Renderer, Enzyme and React Testing Library?

Yes and no. You still have UI to test. But the only place you need UI testing libraries like those are in the View files, and those all consists of pure UI elements, which you arguably may not need to test. The hooks for the ViewController, ViewModel and Model can all be tested with React Hooks Testing Library. What so awesome with that? Well, look at the tiny documentation! It's all you need honestly.

Q: What folder structure would you recommend?

You can see the folder structure I used in this example by looking at the imports and filenames. In general, I would claim there are several folder structures that works, use whatever approach you (and at least some of your colleagues...) find logical.

Q: What if I don't want to use TypeScript?

Well, that's up to you. Just remove the types. They are only there to help.

Q: What if I use React Native?

You can use the same code for React Native as well with just some minor modifications.

Q: How does MVVM help with testing?

Two things decides how much tests will burden or save you. One thing is the architecture of the code. MVVM will by design make the code easier to test, by separating concerns of each layer in the application.

The other thing that helps you is how you structure your tests and what libraries you use to write the tests.

The one guy meme
If you aren't satisfied with the FAQ, I know a guy you can ask

Top comments (17)

Collapse
 
vladislavmurashchenko profile image
VladislavMurashchenko

Good article, good implementation of MVVM in React. But in practice you might don't need it in React in most cases.

The core idea behind MVVM is SRP principle. In React we can reach SRP by splitting components to smaller ones. For example instead of splitting Articles page by 4 layers ( Model, ViewModel, Controller, View) we can split it to CreateArticle and ArticlesList components with corresponding responsibilities. It would be simpler and better from SRP perspective.

Collapse
 
perssondennis profile image
Dennis Persson

I totally agree to this, MVVM is not necessary at all. With the component based architecture and tools like Redux and useSWR we do not need a MVVM structure.

And one can argue not to use MVVM in React. Although, lots of backend developers asks about it. The benefit I see with MVVM though is particularly with testing. Testing is many times half of the development time, and for new developers it can be a lot more than that.

The issue with testing components with a lot of mixed logic is that it requires lots of different mocks and assertions for a single component. What happens is often that developers searches through the code in hope of finding similar mocks they can reuse.

By using the MVVM setup I have described in this article, testing is a piece of cake. Each file will have just a few kind of tests that needs to be written. To learn how to test the code, developers will only have to look how it's done in an adjacent file and can quickly write the tests for all code they develop.

In the end, it's just a matter of taste. MVVM is not the mainstream React way, but with this article I wanted to show a way for how to do it :)

Collapse
 
vladislavmurashchenko profile image
VladislavMurashchenko

From my experience writing tests for components takes much effort and don't provide so much business value.

When there is a peace of complicated logic which need to be tested (for example complicated state transition) I extract it to pure function and test it easily without any mocks.

Thread Thread
 
jamesthomson profile image
James Thomson

Couldn't agree more with this. We practice it this way as well. It's so much easier to test (and refactor) when logic is extracted as pure functions.

Collapse
 
soorajsnblaze333 profile image
Sooraj (PS)

Good article,

But I would prefer to have a different kind of separation in React such as

  • having the fetch wrapper separately
  • all the http requests that use the fetch implementation separately as model functions
  • all the routes stored separately
  • UI view pages separately that would implement these model functions and the UI pages would have component chunks.
Collapse
 
perssondennis profile image
Dennis Persson

Thanks for reading! Let me know if I should post more articles like this by writing a comment or reacting with some of the reactions ❤️🦄 🔥

If you want more memes, you can find those at my Instagram page. Enjoy your day! :)

Collapse
 
carlosjs23 profile image
Carlos A. Escobar

Thanks I'm new to React but coming from backend frameworks like Laravel
where everything is well structured, I was struggling with large React components with everything included inside the view and it's a pain.

Collapse
 
perssondennis profile image
Dennis Persson

Ya, that's a very common thing, backend developers missing the structure they are used to. I often see the web as the Wild West Web since there are so many ways to implement stuff, everyone have their own way to work (at least in frontend).

Truth is though, there are lots of architectural patterns to follow though, just many of them to choose among. For React, Redux has long time served a good structure for scaling applications. With new hooks, things have changed, new structures have been needed.

I would say it works well using MVVM with React. But to backend developers, I would at least recommend trying to grasp what kinds of structures there are for React. They do work very well after all even if it doesn't look like that at a first glance :)

Collapse
 
xenosgrilda profile image
Eduardo L.

Awesome article, thank you for taking your time to write it, I've always struggled to understand how some of those design patters could be applied to front-end applications, it was very interesting to see how you ported MVVM to React.

As someone that has worked with both React and Angular, I couldn't help but to think that, at least the View and ViewController is something that Angular does natively, it's actually their implementation of a component, maybe leaning more to the MVC side.

Thanks for the article again,

Cheers!

Collapse
 
mattbarnicle profile image
Matt Barnicle

This is a very useful article. Thanks for posting it. I've been thinking about how to implement this pattern in React to add more structure to my data components but never taken the time to look into it. I found this to be a good introductory guide. And I would appreciate more articles like this one.

Collapse
 
luciamartyn profile image
Lucia Martyn

Very informative article. Thank you for sharing you knowledge. It has been a constant challenge for me to comprehend the implementation of certain design patterns in front-end applications. Therefore, it was fascinating to observe how you adapted MVVM to React.

Collapse
 
sajidhasan007 profile image
sajidhasan007

Can I get the source code of this project?

Collapse
 
perssondennis profile image
Dennis Persson

I'm sorry to say it doesn't exist. I wrote this article from scratch, by copying and modifying parts from another project. So there's no github repo for this guide.

Collapse
 
c01nd01r profile image
Stanislav

Thank you for the article!
In my opinion, when using MVVM, the separation should be more explicit. Hooks don't contribute to this.
I like how this is implemented in the React VVM library
yoskutik.github.io/react-vvm/

Collapse
 
jwilliams profile image
Jessica williams

MVVM is not a standard pattern in React, but you can implement its principles using Hooks and TypeScript. The basic idea is to separate business logic from presentation by using ViewModel components. These components handle data manipulation and state management while the View components focus on rendering the UI.

Collapse
 
valodyapapikyan profile image
v4lodya

Hi everyone,

I am seeking feedback on the code for my github.com/valodyapapikyan/publish... project