Bringing state control of components to user classes in React
The article describes another variation of the architecture for React applications, which appeared as a result of writing its own class for managing the state of components. The main purpose of the proposed approach is to simplify and accelerate the development of applications for React.
In React applications, data can usually be divided into 2 types: data from the application itself (stored in the store) and data that is used by a particular component when drawing (stored in the state).
On a recent project, I came to the decision to separate the state of the component from it, by implementing my own class for working with the state. In this class, I made the code responsible for updating the component, subscribing to the repositories and receiving data from them. In the store class itself, I implemented the ability to subscribe to its changes, thereby getting rid of global events.
At first I used Reflux on the project, but I quickly felt the following disadvantages:
1) I constantly had to write code to announce new actions and subscribe to them.
2) Problems due to the fact that changing the properties inside the object in state does not cause the component to be updated.
')
In the decision to which I came, there are no shortcomings. Since it showed itself well on a real project, I decided to share it in this article.
The advantages of the proposed architectural approach
1) Less “useless” code that is required to be written in Flux approaches (announcement of actions, subscription to specific actions, and more).
2) Eliminates the need to use event systems.
3) Automatic update of components when updating data in the application repositories.
4) There are no problems when working simultaneously with the same data from the repositories in different components. For example, you can display multiple copies of a component that filters a list with different parameters. Each copy will display its own result, depending on the specified parameters and not dependent on the parameters in other copies of the component.
1) simple-list - an example of receiving data from the repository.
2) list-with-server is the same example, but with receiving data from the server and storing them in the repository.
3) form-editing - an example of a form with editing, binding, server validation and saving data on the server.
4) filters - an example of filtering. It also demonstrates that filtering parameters in one component do not affect the result of filtering in another component of the same type.
I use ObjectPath in my library. ObjectPath allows you to write values indicating the path to the desired field (property) of an object, as well as read and check the existence of a property within an object that stores nested objects.
In addition to the approach itself, the library has implemented:
1) work with server validation. This part was written to work with Django and may not be suitable for projects with server validation on other frameworks.
2) partial and full cancellation of changes - the ability to reset the selected fields in the component state to the values in the repository.
3) solving the problem of updating components when changing properties inside an object in state.
Description of the approach
The main idea is that instead of using standard states in components, use your state object to simplify control of its changes. The standard state of the component has a big drawback - it does not update the component when the nested properties of the object are changed.
Also in this approach, the application has several data warehouses. These storages can be updated from the component and from the outside (for example, when new data is received over the network). The repository updates the UIStates (this is how the states removed from the component are named) subscribed to it. UIState reads the repository data and keeps a copy of it in itself. Before saving in itself, UIState can somehow process the data. After receiving the data from its repository, UIState updates the component. The component can read and write values in a UIState.
Architecture diagram The arrows indicate the direction of data flow.
In this architecture there are the following main entities:
UIState (UI state / state) is the class used instead of the state component. Named so, because it stores the data used by the component to display at the current time. Each component has its own copy of this class. It can subscribe to changes in various repositories, and can also store any other data, just like a regular state component.
When changes, calls setState ({}). Also, when manually changing individual fields, you can specify whether to update the component or not.
This class is already implemented and in most cases you do not need to write your own. If necessary, you can write your own, and also inherit from the default.
Store (storage) - class for storing application data. For example, to store current user data and to store a list of products. Each data type has its own storage. The data in the repository is different from the data in the UIState until the method for storing data in the repository is called, after which the state UIs subscribed to it are updated.
It is also already implemented and in most cases you do not need to write your own. If necessary, you can write your own, and also inherit from the default.
Usually, UIStates are subscribed to change storage, but nothing prevents other classes from subscribing.
Stores - A simple class that stores a list of all application repositories.
Class relationship:
Store - UIState: many to many. As a repository can have many subscribers, so a UIState can be subscribed to multiple repositories.
UIState - Component: one to one. But, a component can also have multiple UIStates. Although this is not necessary.
Anything more you do not need to write. The repository itself will notify subscribers of its changes. UIState in its constructor itself subscribes to the storages transferred to it and updates the component. All the necessary logic is written in 2 classes: DefaultStore, DefaultUIState. In most cases, they are enough, but if necessary, any of them can be replaced with your own, or inherited from them and expand their heirs.
I will describe how to read and write to uiState.
Reading:
let field1 = this.uiState.store_key.field1;
Either let field1 = this.uiState.get ('store_key.field1');
If the data is stored only in the state, without using the Store, then the data is stored in the model object: let field1 = this.uiState.model.field1.
Record:
this.uiState.set ('store_key.field1', newValue).
Again, if the data needs to be stored without using the Store, then use the model: this.uiState.set ('model.field1', newValue).
Example 2 (form with editing, binding, server validation and saving data on the server)
The example is quite large, so the code is not complete here. In addition, it will allow the reader not to be distracted by code that is irrelevant. The video shows an example in action:
1. Creating a repository
import {DefaultStore} from'ui-states'; exportdefaultclassStores{ static currentCustomer = new DefaultStore('currentCustomer'); }
Passing the parent state of the component, and even more so, changing it in the child components in most cases is not worth it. Binding, as in this case, is an exception rather than practice, as it turns out very convenient and besides, the whole form is not redrawn.
Disadvantages of the proposed approach / library
Like any solution, mine also has its drawbacks. In my library, the main drawback is the complexity of further expanding the DefaultUIState class, since it contains a lot of functionality (manual state changes, updating data from storages, updating a specific field from the store, canceling changes, validation).
Perhaps replacing the standard state was unnecessary and instead costing to use HOC or something like mixin.