DEV Community

Kirill Novik
Kirill Novik

Posted on • Updated on

MVU architecture in a React application

Introduction

How to introduce the MVU pattern in a React application?

In the previous articles, we arrived at the conclusion that in order for code to not become legacy we would need to separate our view from business logic in a different way than Redux and Elm are doing it as both approaches not allowing to disentangle view from the business logic completely.

I demonstrated a way to make React a decoupled stateless view similar to how it is done in Elm as part of our quest of trying to make code legacy-proof.

Now, a natural question arises — how about the rest of the pattern? How can we tie everything together?

Elm MVU Pattern

Elm handles UI unidirectional flow using an MVU pattern.

Elm MVU pattern

There is a model — data that represents our application state.

This model is used by the view layer to display the correct state of the UI.

When a user interacts with the UI a message is sent to an update function that returns a new model (new state), which in turn is consumed by the view thus going full circle.

There is another part to it, however, commands that produce side effects as well as subscriptions that seem to unnecessarily complicate this pattern.

Decoupler MVU

In reality, a better abstraction of this would be to make the view as well as all other side effect logic one and the same — after all, they all represent IO agents.

IO agent is really a part of this loop, a non-pure function that takes the model (state), does something with it and the outside world and then returns a message.

In the case of view, these actions are button clicks and other user actions. The user sees a certain screen, does some decision-making, and as a result, there is an interaction that results in a message. (User-UI interaction can be abstracted as an IO agent)

In the case of other IO, like HTTP requests, the IO agent sees some state of the application and based on certain parameters decides to fetch, or not to fetch, something and then sends a message.

This abstraction, where both view, requests, and other non-pure functions are just a form of an IO agent, makes it easy to decouple IO logic from the core of the application and thus allowing us to make the unidirectional flow easy to test, as the update function is just a pure function (that could be tested in a black box testing manner) and IO agents now could be swapped with simplified representations for the ease of testing.

This abstraction also makes sense for the vast majority of the UI applications as the majority of them are IO-bound applications (as opposed to CPU-bound or GPU-bound), where their main purpose is to communicate with various Input-Output components (side-effects) — like HTTP requests, WebSockets, UI-user interaction and many more.

So to simplify this pattern and to allow for more flexibility and decouple components better we could just use this IO-Update pattern.

The unidirectional flow of the application now can be represented as a simple function:

const applicationLoop = async (state: PState): Promise<void> => {
  try {
    const action = await io(state);

    const nextState = update(state, action);

    return applicationLoop(nextState);
  } catch (e) {
    console.error(e);

    return applicationLoop(state);
  }
};
Enter fullscreen mode Exit fullscreen mode

This representation of an application seems to be both simple and natural to think about.

Applying this pattern

In the endeavor of ensuring unidirectional flow, I adopted the MIU pattern and created a class that I named ‘decoupler’. This class connects the view with the rest of the application seamlessly. You can see this in action in the linked application here.

Here is the link to the app where I connected the view and the rest of the application using this pattern.

In addition to this, I incorporated black box tests for the update function and devised a simple IO agent to facilitate server communication. It is important to underscore the significance of these black box tests. They not only ensure the functionality of our code but also provide the flexibility to refactor it. By refactoring, we can introduce necessary design patterns to prevent our logic from becoming convoluted.

The real advantage of this pattern is its flexibility and adaptability. It doesn’t dictate how you implement the various components, thus providing freedom to choose appropriate tools for each part of the application. This flexibility extends to allowing timely refactoring and introduction of design patterns, which are crucial for maintaining legacy-proof code.

Importantly, this flexibility allows for the introduction of code generation and various automation.

Furthermore, the decoupler pattern offers an incremental approach to its implementation. It doesn’t impose any restrictions on how the different parts should be implemented. Therefore, you have the liberty to put Redux behind the update function or customize it to suit your needs.

In essence, this pattern offers highly desirable outcomes — ease of receiving feedback and the ability to incorporate good design patterns. These are both essential components for maintaining legacy-proof code.

Conclusion

In this article, we built on top of the previous article, where we introduced a decoupled view layer, and now considered a practical example of how to implement MVU pattern making this view interactive.

This article also introduced a slightly modified and simplified version of MVU that I term IO-update.

As mentioned in the previous examples, among the advantages that we will consider in future articles is the ability to automate the conversion of designs to code, where the view layer no longer has to be tied to a particular technology as well as the ease of writing black box tests for the update

We now were able to implement an MVU pattern that will allow us to make code legacy-proof.

In the next article, we will put this approach to the test and see where it could get us.

Stay tuned!

Part 6 — From Figma to React, Vue, Svelte, Preact, and Beyond: Exploring the legacy-proof (adaptability) benefits of having a completely separate view that matches designs closely

Useful links

  1. Elm Architecture Explained: This guide introduces you to the Elm Architecture. It’s a good starting point to understand the Model-View-Update (MVU) pattern and how it is implemented in Elm. It can offer insights on how we can incorporate these principles into our React application.

  2. Figma Official Website: Figma is a popular tool for UI/UX design and prototyping. With its collaborative interface, it enables multiple designers to work together in real-time. This can be especially handy when working on a legacy-proof UI project, as it allows for a seamless transition from design to code.

  3. UI Design Handbook by Design+Code: This handbook provides a detailed guide on how to turn your designs into code. It covers everything from working with shapes, icons, and images, to handling typography, layout, and spacing. A perfect resource for developers who want to create pixel-perfect UIs that match the initial designs closely.

  4. Widget Code Generator by Figma: This article introduces a powerful tool provided by Figma — the Widget Code Generator. It automatically generates code for your designs, helping to streamline the development process and reduce the gap between design and implementation. It can be particularly useful for our legacy-proof UI project, enabling faster and more accurate transition from design to code.

Top comments (0)