Applying the MVVM Pattern with React

When creating forms in React, the question of managing the state of the application arises. It would seem that there is a rich choice, but Redux impresses with its fizzbuzz enterprise, and Mobx side effects in OOP code from novice developers. In my experience, both tools are ill-suited for onboarding beginner programmers. And it is very profitable for business to employ them: they are cheap, shy and naive

The solution to the problem will be to throw out unnecessary abstractions from the Frontend codebase. Business logic, spread over several files, drives a novice developer into a stupor. I would like to share one well-established practice.

A long time ago, in a galaxy far, far away…

At the time of 2010, when web application state management moving from non-directional data flow (jQuery) to state containers, early frameworks made extensive use of variations of the MVVM pattern (AngularJS, MarionetteJS, BackboneJS). The fact is that at that time Microsoft was trying to capture the market and was widely promoting silver lightvariety WPF for the internet. And the fight was to the death, for example, markup language XAML was the first to introduce something similar to FlexBox and CSS Grid (cm. StackPanel and Grid). The web copied the developments of the small-soft ones to the maximum.

The mistake that led to the death of Backbone

Backbone’s approach to managing application state was to use a Collection entity that automatically synchronizes the contents of an array with CRUD on the backend side.

Changing the object that lies inside the Collection caused the application to be redrawn. The idea is good, however:

  1. The application programmer must inherit his class from Collection, overriding the methods inside to access the backend. Inheritance, as the generation of an extra abstraction, is an anti-pattern for the frontend, you need to use composition.

  2. Collection provided over 10 methods to override to synchronize content with the backend. As a rule, only one function was redefined syncCollection

  3. When transferring feature refinement from one programmer to another, adding new cases to the function to synchronize the collection was more difficult each time. This is due to the need to maintain backward compatibility with current code.

What can be done?

Can be left on Collection only the task of redrawing the UI, making requests to the backend for reading and writing to user action handlers before data mutation

import { useCollection } from "react-declarative";

const ListItem = ({ entity }) => {

  const handleIncrement = () => {
    /*
    await fetch(`/api/v1/counters/${entity.id}`, {
      method: "PATCH",
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        counter: entity.data.counter + 1
      })
    });
    */
    entity.setData({
      id: entity.id,
      counter: entity.data.counter + 1
    });
  };

  return (
    <div key={entity.id}>
      {entity.data.counter}
      <button onClick={handleIncrement}>Increment counter</button>
    </div>
  );
};

export const App = () => {
  const collection = useCollection({
    onChange: (collection, target) =>
      console.log({
        collection,
        target
      }),
    initialValue: [] // await fetch() or props...
  });

  const handleAdd = () => {
    /*
    const { id, ...data } = await fetch("/api/v1/counters", {
      method: "POST",
    }).then((data) => data.json());
    */
    collection.push({
      id: Math.max(...collection.ids, 0) + 1,
      counter: 0
      // ...data
    });
  };

  return (
    <>
      {collection.map((entity) => (
        <ListItem key={entity.id} entity={entity} />
      ))}
      <button onClick={handleAdd}>Add item</button>
    </>
  );
};

export default App;

The code above outputs a list of counters (demo on codesandbox). Inside the code, the places where you can access the server are commented out. It is also possible to handle the exception in the request so that the block catch will be executed in the context of the form. For example, this can be used to get the snackbar out of a hook notistack

import { useSnackbar } from 'notistack';

...

const { enqueueSnackbar } = useSnackbar();

const handleNetworkRequest = () => {
  fetchSomeData()
    .then(() => enqueueSnackbar('Successfully fetched the data.'))
    .catch(() => enqueueSnackbar('Failed fetching data.'));

An array whose contents need to be synchronized with the backend must be put in the argument initialValue hook useCollection (line 38). The hook will return an object Collectionwhich implements the method map, which allows you to display a list of elements seamlessly with an array. Each element of the array will be wrapped in a container EntityA that provides access to the original value via a property data and method setData. Method call setData (line 17) will synchronously change data and, through debounceask for a hook useCollection redraw the shape

How to synchronize one object?

By analogy with useCollectionthe hook is exported useEntity. It will return the object passed in the arguments, wrapped in Entitychallenge setData the latter will also redraw the form.

By using the above two hooks, you can save on passing callbacks via propsremove an extra boilerplate, do not lose the execution context when splitting interconnected business logic into different files

Where can I see the hook code?

To remove the need to produce dependencies in your project, I provide links to files)

  1. Collection

  2. entity

  3. useCollection

  4. useEntity

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *