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:
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.
Collection provided over 10 methods to override to synchronize content with the backend. As a rule, only one function was redefined
syncCollection
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 Collection
which 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 Entity
A 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 useCollection
the hook is exported useEntity
. It will return the object passed in the arguments, wrapped in Entity
challenge setData
the latter will also redraw the form.

By using the above two hooks, you can save on passing callbacks via props
remove 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)