Register to get access to free programming courses with interactive exercises

MVC JS: Frontend architecture

As mentioned earlier, our way of working with states has one major drawback: handlers are responsible for calling the rendering. Below is an example showing render() being called.

input.addEventListener('change', () => {
  const { registrationProcess } = state;
  if (input.value === '') {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  } else if (!input.value.match(/^\d+$/)) {
    registrationProcess.validationState = 'invalid';
    registrationProcess.errors = ['Bad format'];
  } else {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  }

  render(state);
});

What problems can arise from this approach?

Here it's worth saying that in backend development, such an approach is justified. The backend works within a different paradigm, namely the client-server architecture. In backend development, a handler is essentially a function that either changes a state (this doesn't cause any re-rendering, since it redirects) or retrieves data from the database to generate a response in HTML or JSON form, or others. When it comes to frontend, changes to data will immediately affect what's on the screen.

The example we see above is very simplified; it calls just one render function, which takes the entire state as input. Now imagine that we have dozens of handlers in our application, which still isn't that much, and a large state, which is fairly typical. In this situation, re-rendering everything for every change is quite a costly operation. On the other hand, you can insert a check inside the render function for each chunk of state and monitor whether it's changed. This approach will very quickly become a problem in and of itself. It's easy to forget to check something, it's easy to make a mistake in the check itself, and it's easy to forget to change a check after changing the state structure.

There is another way to accomplish this task. It is based on a concept (they say a design pattern) called the Observer. The idea is very simple. One part of the system observes changes made to another part of the system. If the part being observed has changed, then the observer can do something useful.

In JS, a similar mechanism can be implemented through Proxy, but it's rather complicated. A simpler solution would be to use the off-the-shelf library on-change.

import onChange from 'on-change';

const app = () => {
  const state = {
    ui: {
      value: 'hello',
    },
  };

  const watchedState = onChange(state, (path, value, previousValue) => {
    alert('value changed!');
    console.log(path);
    // => 'ui.value'
    console.log(value);
    // => 'other value'
    console.log(previousValue);
    // => 'hello'
  });

  // After the attribute has been changed, an alert will appear
  const el = document.querySelector('<selector>');
  el.addEventListener('change', () => {
    watchedState.ui.value = 'other value';
  });
}

// Somewhere in another file (usually index.js)
app();

On-change allows you to keep an eye on the desired parts of the state and call the rendering functions when they change. Which parts exactly to listen to and how many "watchers" to hang depends on the task. In primitive situations (or training projects), one watcher for the whole state is enough, but in real situations, multiple watchers make things more convenient (each situation is different).

See the Pen js_dom_mvc_watch by Hexlet (@hexlet) on CodePen.

Handlers now don't know anything about rendering and are only responsible for interacting with the state. In turn, the rendering monitors the state and changes the DOM only as and when it's needed. This way of organizing an application is considered a classic and it's called MVC (Model View Controller). Each word denotes a layer of the application with its own area of responsibility. Model - application state and business logic, View - the layer responsible for interacting with the DOM, Controller - handlers.

Note that Model, Controller, or View are not files, classes, or anything else in particular. These are logical layers that perform their task and interact with each other in a certain way.

Understanding MVC gives an answer to how to structure an application, but it's rarely implemented on its own. Modern frameworks are built on different variations of MVC and have defined the rules of interaction for us. All that remains is to figure them out and follow them.

MVC

The most important thing in this picture are the arrows between the layers. They define the barriers of abstraction. What can interact with what and how, and what can't. For example, in this diagram, there's no arrow from the controller to the view. This means that the controller cannot (can not!) change the view by bypassing the model. What's shown on the screen is a display of the state of the application and nothing else. This kind of code is considered a violation:

// Suppose there's one form on the page
// with a field for entering a task and a button to add it

const form = document.querySelector('form');
const input = document.querySelector('form input');
form.addEventListener('submit', () => {
  watchedState.registrationProcess.state = 'processing';
  // We do something with the data, for example, we add to the state
  input.value = ''; // Clearing the input field directly! MVC violation!
});

There is also no arrow from view to model in the diagram. This means that the view layer cannot change the model while it's running:

const watchedState = onChange(state, (path) => {
  if (path === 'registrationProcess.state') {
    // Status update! MVC violation!
    watchedState.registrationProcess.alert = 'Sending data...';
  }
});

And, of course, the view cannot pretend to be a controller and perform, HTTP requests, for example:

const watchedState = onChange(state, (path, value) => {
  if (path === 'registrationProcess.state') {
    // Make an HTTP request! MVC violation!
    if (value === 'sending') {
      axios.post(endpoint, watchedState.registrationProcess.data);
    }
  }
});

Essentially, the controller does something with the data, and the view layer responds to data changes and changes the DOM.


Recommended materials

  1. Backbone MVC

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

For full access to the course you need a professional subscription.

A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

Bookmate
Health Samurai
Dualboot
ABBYY
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.