How to organize a general 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 but 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 community, a bunch of additional libraries and stateful approaches are used (flux, redux, redux-ations, effector, mobx, cerebral pile of them). 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 which 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 the ancestor’s components causes the entire state to be removed higher and higher and end up in the root component of the App along with logic update this state for all components.


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


    The most common javascript objects and certain rules of 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.jsexportconst AppState = {
     locale: "en",
     theme: "...",
     ....
    }

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


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

    We go further - 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, and the whole chain of links starts from the main entity of the user. Well, in our case, the user object will simply store an array of other entity-stor objects where each object-stor in turn store arrays of other entity-stores.


    Here is an example - there is a business logic that 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 in the status diagram like that:


    exportconst 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


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

    and he will be able to render a list of folders


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

    and each folder component will display a list of projects.


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

    and each component of the project can list tasks


     ....
    <div>{project.tasks.map(task=><Tasktask={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=><Subtasksubtask={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, 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 passes a task object to the child Task component (<div>{project.tasks.map(task=><Task task={task}/>)}</div>) then, due to the fact that objects are stored inside a single object, you can always change this task object outside - for example, AppState.currentUser.folders [2] .projects [3] .tasks [4] .text = "edited task" and then trigger an update of the root component (ReactDOM.render (<App />) and so we get the current state 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 thereby 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 updates the root component.


    exportfunctionupdateDOM(){
      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 let's try to add a bit more convenience to work with the stor. In the example above, you can notice that creating a new entity object every time (for example, project.tasks.push({text: "", subtasks: [], ...})if an object has many properties with default parameters, you will have to list all of them every time and you can make a mistake and forget something etc. The first thing that comes to mind is to create an object to a function where default fields will be assigned and at the same time override them with new data


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

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


    classTask{
      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 to the constructor of the base class.


    classBaseStore{
     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 sorts of other conveniences. 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


    classTask{
     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, you can 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.


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

    Well, so that when 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;
    classBaseStore{
     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 state, 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=><Tasktask={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, the project component must transfer, in addition to the task, the tasks at the same time and their project <Task task={task} project={this.props.project}/>. And if you suddenly need to paint the task in color common for 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 dependency that a component should be aware of what its nested components require. 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 superior 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 possibility 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 should 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 should have a link to the folder object, etc., up to the root AppState object. So the component no matter how deep it is, you can always follow the parent objects by reference 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 creating 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, as business logic increases, 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, if you move a project from one folder to another, you will need to not only update the project.folder = newFolder property but also delete themselves from an array of projects previous folder and adding an array of projects for the new folder) begins to repeat itself, and it is also possible to make the base class so that when an object was enough to indicate a parent - new Task({project: this.porps.project})a base class automatically added to the new object in the array project.tasks, and also when transferring the problem to another project would be enough to simply update the Fieldtask.update({project: newProject})and the base class would automatically delete the task from the array of tasks of the previous project and add it 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 is, why do we need libraries to manage the state, and in particular mobx?


    The fact is that in the described approach, the organization of the general state when using ordinary native vanilla js objects (or objects of classes) has one big drawback - if a small part of the state or even one 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 recursively compare the virtual home of the entire application given 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 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-referer practically without changing anything in the application - just add the decorator to the fields of the class @observableand the components that render these fields to another component@observer and it remains to cut only the 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 a part of the state or even a single field will update only those components that are signed (apply inside the render () method) to a specific field state object.


    Also popular now: