DEV Community

aon24
aon24

Posted on

React, OOP and key-value store. Two levels of MVC

The article is long and without pictures. This is not an instruction how to do it. These are my thoughts on using React in large projects. Before reading it makes sense to look at the result (without ads).

Sorry my bad english

1. Server (1st level MVC).

From the client, a request is sent to the server's http-API, the response to which should be an html page. The API parses the parameters, checks the ACL and makes a request to the model with the necessary parameters.

Model (level 1).

The model returns an object containing the names and values of the fields (key-value).
Further, the API calls the desired view, i.e. form. The name of the form can be in the parameters of the http-request (... & form = myFormForPrint & ...), or it can be generated by the model from the database (among other things, the key-value can be 'form': 'myForm'). The name of the form in the request, of course, is more priority.

Form as a view of the 1st level.

My form is a Python module (hereinafter referred to as the form module), which contains markup and several functions. It was more difficult to make a class out of each form (“Simple is better than complex”, although “a class without creating a class” sounds strange).
The view creates an html page that contains:

  1. The modified object obtained from the model in the js-form.
  2. A link to the markup (my markup is a Python dictionary converted to a json string).
  3. Links to additional js and css files needed only by this form.
  4. Link to the SPA, i.e. on the React-App (transpiler that will render the markup).

The same is more detailed.

Formation and modification of data.

The form module can contain a queryOpen function that passes an object from the model, a mode (read/edit/new/etc), a user name, parameters from an http request.
queryOpen can change / add / remove fields (eg, replace 'who': 'Ivan Petrov' with 'who': 'to Ivan Petrov' or add a new 'toWhom' field: 'to Ivan Petrov').
If a client receives a request to create something new ('new' mode, for example, a new order), queryOpen will receive an empty object and will be able to set the default field values.
For sending to the client, the dictionary 'data', also key-value: {'fieldName1': value1, 'fieldName2': value2, ...} will be prepared.
Those interested can call it a layer, pipeline, script, serializer DRF.

Page layout.

The form module must have a function page, which is passed mode, user name and parameters from the http-request (almost as in queryOpen). Depending on the transmitted function forms the desired markup. Here is React, not JQuery, so no Jinja 2 and other templates. If you have made your SPA for this form, then you should include a link to this script in the html. If all forms (in my project there are about 100 of them) are created by one script, you can register the link statically.
My react-application parses and draws the form received as a json-string (more on this below). The page function on the first call forms and remembers this string, on subsequent calls it returns it. The url to this line is cached on the client.
So The view of the 1st level generates an html page containing the fields data and a link to the markup (to the form).
What is the difference between a template and a form? From the point of view of the programmer - no. There are markup and markup there. From the point of view of conceptual analytics, there is a difference: in the case of DJango, the view is no longer a controller, it does not form a page, it simply gives a link to the json line with markup. In terms of speed, there are advantages: the server does not generate html with each request, the browser stores the form in the cache, only data is transmitted over the network.

1st level controller.

The controller is a client. The API sent him an html page that the view of the 1st level formed and forgot. If the client wants to save the data in the database, he will remind you. The sequence is reverse:
the client sends in json-format field values,
the form module calls the querySave function. With it, you can do additional validation, change something in the data.
If querySave returns True, the data is transferred to the model for writing to the database.
Upon successful completion of the recording, the client receives the long-awaited code 200.
In the form module there can be a function postSave, called after writing to the database and responding to the client. It can send data for indexing, etc.

MVC is good because it shares responsibility. The model returned data and does not interfere with the submission action. It happens that the model returns the finished html markup. For such markup there is a wonderful form that does nothing, just sends to the controller what it has received. The customer wanted to replace the table on the schedule - no problem. We delete the form, make a new one at least for React, at least for Plotly-Dash and send it to the controller. Let him find out for himself.

Replacing the controller of the 1st level is even easier, because the controller is a client, i.e. employee at the computer. If the client began to fail, to make mistakes when filling in the fields, they fired and took another.

2. Client (2nd level MVC)

The client receives the data in the form of keys-values, we will consider it a model, and the url of the form, it is also a view.
In my case, the form is the json string in which the controllers and markup elements are packed. Each controller has a name (field name). He is intelligent (keeps the state in himself), handsome and object-oriented.

Why is that? Why not redux? I explain.

There is a project, in the project there are about 100 forms and a lot of business logic. There are about 500 active users with their own wishes. There is a task: to make everything quickly and quickly make changes.
You can describe each form in the React and do the processing of shares in the reducer. As a result, we get:

  • multi-megabyte SPA, or several SPA
  • form development / revision - new version of SPA
  • change of business logic of the form - new version of SPA
  • branchy, extending beyond the horizon, tree of reducer
  • any changes made by the React-programmer

We went the other way. Each form that is open on the screen is an object of the Document class (let's call it doc). After downloading doc has:

  • object with fields in the form of key-value
  • form url
  • pure javascript and css for this form

The doc object, via url, receives a form (json string) from the server or from the cache, parses it into React-components and sends it to render. When rendering in doc, a register of controllers {fieldName: control, ...} is created.
I hope ref is not prohibited. Prohibiting ref is the same as prohibiting pointers in pluses.

2st level controller.

The controller is registered in the registry of the doc object under the field name

  • keeps state in itself
  • the controller may have methods getValue, setValue, setFocus, changeDropList
  • the controller can generate a recalc event when the state changes
  • a button-type controller does not have a state, but can generate commands.

Middleware

If there is a controller, there is a view, then there should be a middleware. Controllers with lists are able to update lists from the server themselves (the changeDropList method), but in many cases you cannot do without middleware.
A class Document can handle a dozen standard commands (open, close, print with a selection of the form, etc.), for other commands loadable scripts are used. For each form of their own. There is middleware.

The React application initially knows nothing about the forms that it will have to maintain and where the form scripts will be located. It will know all this when it receives the form url. To ensure the connection of loadable form scripts with objects of the Document class, 1 global (horror) variable is introduced. The script for each form should start like this:

window.sovaActions = window.sovaActions || {};
window.sovaActions.myform = {

, where myform is the name of the form in lower case. The form name is needed because On the page can be opened several documents at the same time.
Script structure:

window.sovaActions = window.sovaActions || {};
window.sovaActions.myform = {
    init: doc => {}, // initialization of data. Called before rendering (controllers have not yet been created). Rarely used.
    Init2: doc => {}, // Called after rendering. I'm not used at all.

    cmd: {// command handlers
    logoff: doc => {window.location.href = '/ logoff'},
         ...
    },
    recalc: {// state change handlers
        PROJECTO: doc => doc.forceUpdate (),
    // called when the projectO checkbox changes
    // controls the hiding of the project section
         ...
    },
    hide: {
       project: doc =>! doc.getField ('projectO'),
    // hide everything with the name project, if the 'projectO' field is empty
         ...
    },
    readOnly: {
        SENTOK: doc =>! Doc.getField ('MODIFIED'),
// field 'SENTOK' in readOnly mode, if the field 'MODIFIED' is empty
         ...
    },

    validate: {
        who: doc => doc.getField ('who')? '': 'The' Who 'field is not filled'
// check the field who. Empty line - OK, otherwise the message to the operator


// form validation is a promise. In this example, yes () is always returned.
        form: doc => new Promise ((yes, no) => {
            let disableAutoOrder = doc.getField ('ccType') === 'query';
            for (let i = 1; i <= 5; i ++) {
                let val = doc.getField ('RESPRJ' + i);
                if (/ behind my signature / .test (val) &&! / (inform | inform) \ s + (for each | each)? \ s * applicant / .test (val)) {
                    disableAutoOrder = true;
                }
            }
            disableAutoOrder && doc.setField ('AUTOORDER', '');
            yes ();
        })
         ...
    },
};

In all functions, the first parameter is a reference to the doc object. Even if there are several identical forms on the page, there will be no confusion in the teams.
In general, it looks like a redux-form, but (with all due respect) is not such a universally-useless.

Many people understand SPA simply. Said "SPA!", Then shove it all on 1 page. This is mistake.

Example of an unsuccessful SPA: Sber-online bank.

I click "New payment", the form opens on the same tab (even with ctrl). I cannot open an old payment order to copy something from there. I can't do anything at all until I leave editing. Perhaps the programmer did not know that you can open another 1 SPA in a new tab.

The people who read it here have noticed correctly:

"React+Node.js. I in the form of corrected, press ctrl+s, and on the second monitor I saw the result. But to understand your pythonturds - dismiss."

It's hard to argue with that, but I will try.

  1. React + ES2016 is fine, but Python is better.

It doesn't sound convincing.

  1. On python, you can write a function that returns a fragment of the form, and insert a fragment where necessary.

On the reactor is also possible. Import has not been canceled.

  1. On the python form is more flexible. Depending on the user's access scheme, I can offer him the desired form. Deal with the rights and roles better on the server. When hundreds of people with conflicting requirements work with the system, the interface often changes. On python, this takes minutes, without having to reassemble the application. My SPA has not been updated for months, and the forms have changed very often.

Rebuild the application for a few minutes.

  1. There is a 2-screen LM - layout manager, on one screen you place elements (tags, controls, fragments), on the other in real time you can see the result (websocket is used). It does not work on a mobile phone, but the development of forms on a smartphone is still rare. The programmer at React is expensive and rare, and, having LM, you can assign the arrangement of elements in the form to anyone.

Have saved on the React-programmer, hired a Python-programmer. It is not known who is cheaper.

Not convinced.

I remember the discussion, where the programmer complained about the confusion of the reducers, and everyone who felt like it, pecked him. Comrade had in SPA 4 (FOUR!) form.

There are about 100 of them in my project. My friend has about 200.
If you push all the forms in 1 SPA, it will be very cumbersome. It can be divided into several, but in my opinion, managing such a project is more difficult. There is code splitting, but it will complicate things even more and will require 5 of senior in the project.

I love the reactor. The persistent hatred of javascript and html disappeared when I saw how beautifully, in the OOP style, declarative html and imperative javascript came together. But redux did not inspire me, because it kills the OOP. Among my controls is FileShow. This is a container with silly components. The file list is stored in a container, and stupid components only show the name and are able to play, preview, upload, download and callback. Works great without mapPropsToState.

People are different: some oil verbs, some oil nouns.
At the heart of Redux is the action, i.e. verb. And the reducer is essentially a verb that manipulates a component.

This approach is called functional programming, this is normal (I myself use functions), but I think with nouns (objects), when properties are encapsulated, there are signals and open methods for communicating with the outside world. React components ideally match OOP, unless you use connect, which kills a component as a class.

Freud and Jung got differently explained motivation, while both were practicing doctors, both treated (each in its own way) and, most importantly, cured people. If your application works and is liked by users, spit on the lack of a conceptual model, not matching any models there, even if it is written in JQuery. It does not matter. The main thing is the result.

And further. Designing a system on React is a bug. It is necessary to design in the head. React is an excellent tool for building an interface, but the priority in designing is level 1. The client part for MVC Level 1 is just a controller, and it can be implemented by several libraries. The same example with the table: you need numbers - the SPA opened in the React, need a chart - the component from Plotly-Dash opened.

Creating a SPA for working with a database does not mean rewriting all the old forms. In my project there are forms born in 2012, and no one is going to rewrite them.

I'm talking about serious projects. If you have 4 forms, design as you want, and everything will work out.


Why in the header key-value store, and not the key-value database?

Because we implemented KV in the RDB as 1 table of 5 fields:

  1. uuid - BINARY, 16 bytes. Unid document.
  2. xcrt - TIMESTAMP. Date-time the field was created.
  3. xnam - text. The field name (key).
  4. xval - text. The field value.
  5. xmdf - TIMESTAMP. Date-timewhen the field was transferred to history.

When the field changes, the old value is transferred to the history and a new record is created. Such is the time machine with the ability to roll back.

For the search they used Sphinx Search (simple, convenient, fast), several tables were added to display the lists.

We do not have a big date: on 2 servers there are about 1000 journals, the number of documents in the journal is from 1 to 600 thousand. On one MySQL server, on another PostgreSQL. Both cope. Easier than Mongo and faster.

With respect,
Alexey Nosikov

Top comments (0)