How to organize a common state in react-applications without using libraries (and why mobx is needed)

Immediately a small spoiler - the organization of the state in mobx is no different from the organization of the general state without using mobx on a clean reactor. The answer to the legitimate question why then actually this mobx is needed you will find at the end of the article for now the article will be devoted to the organization of the state in a pure react-application without any external libraries.




A reaction provides a way to store and update the state of components using the state property on the class component instance and the setState method. But nevertheless, among the community’s community, a bunch of additional libraries and stateful approaches are used (flux, redux, redux-ations, effector, mobx, and a cerebral pile). But is it possible to build a large enough application with a bunch of business logic with a large number of entities and complex data relationships between components using only setState? Is there a need for additional libraries to work with the state? Let's see.

So we have setState and that updates the state and causes the component to re-render. But what if the same data is required by many components that are not related to each other? In the official dock of the reactor there is a section "lifting state up" with a detailed description - we simply raise the state to the ancestor common for these components by passing through the props (and through intermediate components if necessary) data and functions for changing it. In small examples, this looks reasonable, but the reality is that in complex applications, there are probably a lot of dependencies between components and the tendency to make states common for ancestor components leads to the fact that the entire state will be taken out higher and higher and end up in the root component of the App along with logic update this state for all components. As a result, setState will be encountered only for updating local data for a component or in the root component of the App in which all logic will be concentrated.


But is it possible to store, process and render the state in the reactor application using neither setState, nor any additional libraries and provide general access to this data from any components?


The most common javascript objects and certain rules for their organization come to our aid.


But first you need to learn how to decompose applications into entity types and their relationships with each other.


To begin with, we will enter an object that will store global data that relates to the entire application - (these can be settings for styles, localization, window sizes, etc.) in a single AppState object and simply move this object into a separate file.


// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... } 

Now in any component you can import and use data from our site.


 import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> ) 

Go ahead - almost every application has the essence of the current user (so far it doesn’t matter how it is created or comes from the server, etc.) therefore, there will also be some singleton user object in the state of our application. It can also be moved to a separate file and also imported, and can be stored right inside the AppState object. And now the main thing - you need to determine the entity schema of which the application consists. In terms of the database, these will be tables with one-to-many or many-to-many connections, with the entire chain of connections starting from the main entity of the user. Well, in our case, the user object will simply store an array of other entity-stores where each object-store in turn stores arrays of other entity-stores.


Here is an example - there is a business logic which is expressed as "a user can create / edit / delete folders, projects in each folder, tasks in each project and subtasks in each task" (something like a task manager is obtained) and will look like in the status diagram like that:


 export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } } 

Now the App’s root component can simply import this object and render some information about the user, and then can transfer the user object to the dashboard component


  .... <Dashboard user={appState.user}/> .... 

and he will be able to render a list of folders


  ... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ... 

and each folder component will display a list of projects.


  .... <div>{folder.projects.map(project=><Project project={project}/>)}</div> .... 

and each component of the project can list tasks


  .... <div>{project.tasks.map(task=><Task task={task}/>)}</div> .... 

and finally, each task component can render a list of subtasks by passing the desired object to the subtask component


  .... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> .... 

Naturally, no one will display all the tasks of all projects of all folders on one page, they will be divided into side panels (for example, for a list of folders), by pages, etc., but the general structure is approximately the same - the parent component renders the nested component by passing an object with data. An important point to note is that any object (for example, an object of a folder, project, task) is not stored inside the state of any component - the component simply receives it through the props as part of a more general object. And for example, when a project component sends a task object ( <div>{project.tasks.map(task=><Task task={task}/>)}</div> ) to the child component Task, due to the fact that objects are stored inside a single object You can always change this task object from the outside - for example, AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "edited task" and then trigger the update of the root component (ReactDOM.render (<App /> ) and so we get the current status of the application.


Next, let's say we want to create a new subtask when clicking on the "+" button in the Task component. Everything is simple


  onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() } 

since the Task component receives a task object as a props and this object is not stored inside its state, but is part of the global AppState store (that is, the task object is stored inside the task array of a more general project object and that in turn is part of the user object and the user is already stored inside AppState ) and thanks to this connectivity, after adding a new task object to the subtasks array, you can trigger an update of the root component and thus update and update the home for all data changes (no matter where they occurred) simply by calling the upd function ateDOM which in turn simply performs the update of the root component.


 export function updateDOM(){ ReactDom.render(<App/>, rootElement); } 

And it does not matter what data of which parts of AppState and from which places we change (for example, you can send the folder object through the props through the intermediate Project and Task components to the Subtask component and that can simply update the folder name (this.props.folder.name = "new name ") - due to the fact that the components receive data through the props, updating the root component will update all the nested components and update the entire application.


Now we will try to add some amenities of working with the stor. In the example above, you can see that creating a new entity object each time (for example project.tasks.push({text: "", subtasks: [], ...}) if an object has many properties with default parameters, then every time you have to list them and you can make a mistake and forget something, etc. The first thing that comes to mind is to put the creation of an object into a function where default fields will be assigned and at the same time to override them with new data


 function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } } 

but if you look at it from the other side, then this function is the constructor of a certain entity, and javascript classes are great for this role


 class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } } 

and then the creation of the object will simply create an instance of the class with the ability to override some default fields


 onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) } 

Further, it can be noted that creating classes for project objects, users, subtasks in the same way, we will get duplication of code inside the constructor.


 constructor(){ Object.assign(this,data) } 

but we can take advantage of the inheritance and render this code in the constructor of the base class.


 class BaseStore { constructor(data){ Object.update(this, data); } } 

Then you can see that every time we update a state, we manually change the fields of the object.


 user.firstName = "..."; user.lastName = "..."; updateDOM(); 

and it becomes difficult to track, drill down and understand what is happening in the component and therefore there is a need to define a certain common channel through which any data will be updated and then we will be able to add logging and all other amenities. To do this, the solution will be to create an update method in the class that will accept a temporary object with new data and update itself and establish a rule that you can update objects only through the update method and not by direct assignment


 class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } } //// user.update({firstName: "...", lastName: "..."}) 

Well, in order not to duplicate the code in each class, we also move this update method to the base class.


Now you can see that when we update some data we manually have to call the updateDOM () method. But for convenience, it is possible to perform this update automatically — every time the update ({...}) method of the base class is called.
The result is that the base class will look something like this.


 class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } } 

Well, so that if you consistently call the update () method, there are no unnecessary updates, you can postpone the component update to the next event cycle


 let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } } 

Then you can gradually increase the functionality of the base class - for example, in addition to updating the status, you can manually send a request to the server each time you call the update ({..}) method in the background to send a request. It is possible to organize a live update channel on the web socket by adding an account of each created object in the global hash map without changing the components and working with the data at all.


There is much more that can be done but I want to point out one interesting topic - very often passing an object with data to the desired component (for example, when a project component renders a task component -


 <div>{project.tasks.map(task=><Task task={task}/>)}</div> 

the task component itself may need some information that is not stored directly inside the task, but is located in the parent object.


Let's say you need to paint all tasks in the color that is stored in the project and is common to all tasks. To do this, in addition to the task, the project component must also submit its own project <Task task={task} project={this.props.project}/> . And if you suddenly need to paint the task in color common to all tasks of one folder, you will have to transfer the current folder object from the Folder component to the Task component by forwarding through the intermediate Project component.
There is a fragile dependence that a component needs to know about what its nested components need. Moreover, the possibility of the context of the reactor, although it will simplify the transfer through intermediate components, will still require a description of the provider and knowledge of what data is needed for the child components.


But the most important problem is that with every revision of the design or change of the customer's wishes when the component needs new information, it will be necessary to change the higher-level components either by forwarding the props or creating context providers. I would like the component to receive an object with data through the props to somehow access any part of our application state. And here the opportunity of javascript (as opposed to any functional languages ​​like elm or immutable approaches like redux) fits perfectly - so that objects can store circular references to each other. In this case, the task object must have a task.project field with a link to the object of the parent project in which it is stored, and the project object in turn must have a link to the folder object, etc., up to the root object AppState. Thus, the component, no matter how deep it is, can always follow the link through the parent objects and get all the necessary information and do not need to pass it through a bunch of intermediate components. Therefore, we introduce a rule - every time you create an object, you need to add a link to the parent object. For example, now creating a new task will look like this.


  ... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask); 

Further, with an increase in business logic, you can see that the backplate associated with supporting backlinks (for example, assigning a link to the parent object when creating a new object or for example, when transferring a project from one folder to another will require not only updating the project.folder = newFolder property but deleting yourself from the array of projects of the previous folder and adding a new folder to the array of projects) begins to repeat and it can also be transferred to the base class so that when creating the object it was enough to specify the parent - new Task({project: this.porps.project}) new Task({project: this.porps.project}) and the base class would automatically add a new object to the project.tasks array and also when transferring the task to another project, it would be enough just to update the task.update({project: newProject}) field and the base class would automatically delete the task from array of tasks of the previous project and added to the new one. But this already requires declaring relationships (for example, in static properties or methods) so that the base class knows which fields to update.


Conclusion


In such a simple way using only js-objects we came to the conclusion that you can get all the convenience of working with the general state of the application without introducing into the application dependence on the external library to work with the state.


The question arises, why do we need libraries to manage the state and in particular mobx?


The fact is that in the described approach of organization of the general state when using ordinary native vanilla js objects (or class objects) there is one big disadvantage - if a small part of the state or even a single field changes, an update or "rerender" of components that are not connected and do not depend on this part of the state.
And on large applications with bold ui, this will lead to brakes because the reactor simply does not have time to compare the virtual home of the entire application recursively considering that in addition to the comparison, each tree will generate a new tree of objects describing the layout of absolutely all components.


But this problem, despite its importance, is purely technical - there are libraries similar to the vitual dom reactor that better optimize the perener and can increase the limit of components.


There are more effective techniques for updating a house rather than creating a new tree of a virtual house and the subsequent recursive comparison passage with the previous tree.


And finally, there are libraries that are trying to solve the problem of slow updating through a different approach - namely, to shake which parts of the state are associated with which components and, when changing some data, calculate and update only those components that depend on this data and do not touch the other components. Such a library is also redux, but it requires a completely different approach to state organization. But the mobx library, on the contrary, doesn’t add anything new and we can get the acceleration of the re-reder practically without changing anything in the application - just add the @observable decorator to the class fields and the @observable decorator to the components that render these fields and left cut only unnecessary update code of the root component in the update () method of our base class and we will get a fully working application, but now changing part of the state or even one field will update only those components which matured signed (turning inside method render ()) for a particular field of a particular state of the object.

Source: https://habr.com/ru/post/416361/


All Articles